Un portátil con una bombilla y diversos engranajes alrededor.

Ejercicios prácticos: mejoras para la Clase ListaArray 🚀

En una práctica anterior, aprendiste a implementar una clase ListaArray básica que simula el comportamiento de un ArrayList. Hoy vamos a dar un paso más y analizar posibles mejoras para optimizarla, hacerla más robusta y adaptarla a casos de uso más avanzados. Estas mejoras no solo te ayudarán a comprender cómo perfeccionar tus clases, sino que también son un ejercicio clave para aprender buenas prácticas en programación orientada a objetos.


Problemas y Limitaciones Actuales de ListaArray

  1. Falta de Flexibilidad para Otros Tipos de Datos:
    • Actualmente, ListaArray solo funciona con int. Esto limita su uso, ya que no puedes almacenar otros tipos de datos como String, double o incluso objetos.
  2. Capacidad Inicial Fija:
    • Aunque la capacidad crece dinámicamente, comienza siempre en 10, lo que puede no ser ideal para todas las aplicaciones.
  3. Falta de Métodos Útiles:
    • No incluye métodos comunes como:
      • contains(elemento): Para verificar si un elemento está en la lista.
      • indexOf(elemento): Para obtener el índice de un elemento.
      • clear(): Para vaciar la lista.
  4. Eliminación Ineficiente:
    • El método remove mueve manualmente los elementos después del índice eliminado. Esto puede ser lento si hay muchos elementos.
  5. Falta de Manejo de Excepciones Más Específicas:
    • Actualmente, usamos IndexOutOfBoundsException, pero podríamos ofrecer mensajes más detallados o manejar otros tipos de errores.
  6. No Implementa Interfaces Estándar:
    • ListaArray no implementa ninguna interfaz como List, lo que dificulta su uso en conjunto con otras estructuras de datos de Java.

Mejoras Propuestas para la Clase ListaArray

1. Hacerla Genérica con clases genéricas (Generics)

Problema: La clase solo admite int.
Solución: Usar generics para permitir que ListaArray almacene cualquier tipo de dato.

Código Mejorado:

import java.util.Arrays;

/**
 * Clase que implementa una lista dinámica (similar a un ArrayList) utilizando un array.
 * Permite almacenar elementos de cualquier tipo (genérico).
 * @param <T> el tipo de elementos que almacena la lista.
 */
public class ListaArray<T> {
    /**
     * Array que almacena los elementos de la lista.
     * Es de tipo genérico T, lo que significa que puede almacenar cualquier tipo de objeto.
     * Se inicializa con una capacidad inicial de 10.
     */
    private T[] elementos;
    /**
     * Variable que mantiene el número de elementos que realmente hay en la lista.
     * No es lo mismo que la capacidad del array.
     */
    private int size;

    /**
     * Constructor de la lista. Inicializa el array con una capacidad inicial de 10.
     */
    @SuppressWarnings("unchecked") // Ignoramos la advertencia de casting inseguro. Sabemos que es seguro en este contexto.
    public ListaArray() {
        elementos = (T[]) new Object[10]; // Creamos un array de Objects y lo casteamos a T[].  Es la forma de crear arrays genéricos en Java.
        size = 0; // Inicializamos el tamaño a 0 porque la lista está vacía.
    }

    /**
     * Añade un elemento al final de la lista.
     * Si el array está lleno, se llama al método expandirCapacidad() para aumentar su tamaño.
     * @param elemento el elemento a añadir.
     */
    public void add(T elemento) {
        // Comprobamos si el array está lleno.
        if (size == elementos.length) {
            // Si está lleno, expandimos su capacidad.
            expandirCapacidad();
        }
        // Añadimos el elemento a la siguiente posición disponible.
        elementos[size] = elemento;
        // Incrementamos el tamaño de la lista.
        size++;
    }

    /**
     * Método privado que expande la capacidad del array al doble.
     * Se llama cuando se añade un elemento y el array está lleno.
     */
    private void expandirCapacidad() {
        // Creamos un nuevo array con el doble de capacidad.
        @SuppressWarnings("unchecked")
        T[] nuevoArray = (T[]) new Object[elementos.length * 2];
        // Copiamos los elementos del array antiguo al nuevo.
        System.arraycopy(elementos, 0, nuevoArray, 0, elementos.length);
        // Asignamos el nuevo array al atributo elementos.
        elementos = nuevoArray;
    }

    /**
     * Obtiene el elemento que se encuentra en una posición determinada.
     * @param indice la posición del elemento a obtener.
     * @return el elemento que se encuentra en la posición indicada.
     * @throws IndexOutOfBoundsException si el índice está fuera de los límites de la lista (es decir, si es menor que 0 o mayor o igual que el tamaño de la lista).
     */
    public T get(int indice) {
        // Comprobamos si el índice está dentro de los límites.
        if (indice < 0 || indice >= size) {
            // Si está fuera de los límites, lanzamos una excepción.
            throw new IndexOutOfBoundsException("Índice fuera de rango");
        }
        // Devolvemos el elemento que se encuentra en la posición indicada.
        return elementos[indice];
    }

    /**
     * Obtiene el número de elementos que contiene la lista.
     * @return el número de elementos que contiene la lista.
     */
    public int size() {
        return size;
    }

    /**
     * Elimina el elemento que se encuentra en una posición determinada.
     * @param indice la posición del elemento a eliminar.
     * @throws IndexOutOfBoundsException si el índice está fuera de los límites de la lista.
     */
    public void remove(int indice) {
        // Comprobamos si el índice está dentro de los límites.
        if (indice < 0 || indice >= size) {
            // Si está fuera de los límites, lanzamos una excepción.
            throw new IndexOutOfBoundsException("Índice fuera de rango");
        }
        // Desplazamos los elementos que están a la derecha del elemento eliminado una posición hacia la izquierda.
        System.arraycopy(elementos, indice + 1, elementos, indice, size - indice - 1);

        elementos[size - 1] = null; // Ayuda al Garbage Collector a liberar la memoria del objeto eliminado (opcional, pero buena práctica).
        // Decrementamos el tamaño de la lista.
        size--;
    }

        /**
     * Devuelve una representación en cadena de la lista.
     * @return una cadena con los elementos de la lista.
     */
    @Override
    public String toString() {
        return Arrays.toString(Arrays.copyOf(elementos, size));
    }
}

Ventaja: Ahora puedes usar ListaArray con cualquier tipo de dato:

ListaArray<String> listaStrings = new ListaArray<>();
listaStrings.add("Hola");
listaStrings.add("Mundo");
System.out.println(listaStrings.get(1)); // Mundo


2. Personalizar la Capacidad Inicial

Problema: La capacidad inicial es fija.
Solución: Permitir que el usuario especifique la capacidad inicial al crear la lista.

Código Mejorado:

/**
 * Constructor de la lista que permite especificar la capacidad inicial del array.
 *
 * @param capacidadInicial la capacidad inicial del array. Debe ser un valor mayor que 0.
 * @throws IllegalArgumentException si la capacidadInicial es menor o igual a 0.
 */
public ListaArray(int capacidadInicial) {
    // Comprobamos si la capacidad inicial es válida.
    if (capacidadInicial <= 0) {
        // Si no es válida, lanzamos una excepción IllegalArgumentException.
        // Esta excepción indica que se ha pasado un argumento ilegal al método.
        throw new IllegalArgumentException("La capacidad inicial debe ser mayor que 0");
    }
    // Si la capacidad inicial es válida, inicializamos el array con esa capacidad.
    @SuppressWarnings("unchecked") //Suprimimos el warning porque sabemos lo que hacemos.
    elementos = (T[]) new Object[capacidadInicial];
    // Inicializamos el tamaño a 0, ya que la lista está vacía al principio.
    size = 0;
}

Ventaja: Puedes optimizar el uso de memoria en aplicaciones que requieran listas más grandes o pequeñas desde el principio.


3. Agregar Métodos Útiles

Problema: Faltan métodos comunes.
Solución: Implementar métodos adicionales.

Métodos Nuevos:

/**
     * Comprueba si la lista contiene un elemento específico.
     *
     * @param elemento el elemento que se va a buscar.
     * @return true si la lista contiene el elemento, false en caso contrario.
     */
    public boolean contains(T elemento) {
        // Iteramos sobre los elementos de la lista.
        for (int i = 0; i < size; i++) {
            // Comparamos el elemento actual con el elemento buscado usando el método equals().
            // Es importante usar equals() para comparar objetos, no ==.
            if (elementos[i].equals(elemento)) {
                // Si encontramos el elemento, devolvemos true.
                return true;
            }
        }
        // Si hemos recorrido toda la lista y no hemos encontrado el elemento, devolvemos false.
        return false;
    }

    /**
     * Busca la primera aparición de un elemento específico en la lista.
     *
     * @param elemento el elemento que se va a buscar.
     * @return el índice de la primera aparición del elemento en la lista, o -1 si el elemento no está presente.
     */
    public int indexOf(T elemento) {
        // Iteramos sobre los elementos de la lista.
        for (int i = 0; i < size; i++) {
            // Comparamos el elemento actual con el elemento buscado usando el método equals().
            if (elementos[i].equals(elemento)) {
                // Si encontramos el elemento, devolvemos su índice.
                return i;
            }
        }
        // Si hemos recorrido toda la lista y no hemos encontrado el elemento, devolvemos -1.
        return -1; // No encontrado
    }

    /**
     * Elimina todos los elementos de la lista, dejándola vacía.
     * La capacidad del array se mantiene, pero el tamaño se reduce a 0.
     */
    public void clear() {
        // Re-creamos el array para que todos los elementos anteriores sean elegibles para el Garbage Collector.
        @SuppressWarnings("unchecked")
        elementos = (T[]) new Object[elementos.length]; // Conservamos la capacidad original.
        // También se podría hacer un bucle y asignar null a cada elemento, para ayudar al Garbage Collector.
        // size = 0; // Reiniciamos el contador de elementos.
        size = 0; // Reiniciamos el contador de elementos.
    }

Ventaja: Estos métodos facilitan tareas comunes como buscar o vaciar la lista.


4. Mejorar la Eficiencia de remove

Problema: Mover los elementos uno por uno puede ser lento.
Solución: Usar System.arraycopy para copiar los elementos.

Código Mejorado:

/**
     * Elimina el elemento que se encuentra en una posición determinada (índice).
     *
     * @param indice la posición del elemento a eliminar.
     * @throws IndexOutOfBoundsException si el índice está fuera de los límites de la lista (es decir, si es menor que 0 o mayor o igual que el tamaño de la lista).
     */
    public void remove(int indice) {
        // 1. Validación del índice:
        // Comprobamos si el índice proporcionado está dentro de los límites de la lista.
        // Si el índice es negativo o mayor o igual que el tamaño de la lista, lanzamos una excepción.
        if (indice < 0 || indice >= size) {
            throw new IndexOutOfBoundsException("Índice fuera de rango");
        }

        // 2. Desplazamiento de elementos:
        // Usamos System.arraycopy() para desplazar los elementos que están a la derecha del elemento que se va a eliminar,
        // una posición hacia la izquierda.  Esta es una forma eficiente de "rellenar el hueco" que deja el elemento eliminado.
        //
        // Explicación detallada de System.arraycopy():
        // - elementos: El array origen (el mismo array 'elementos' en este caso).
        // - indice + 1: La posición inicial en el array origen desde donde se copian los elementos (el elemento *después* del que se elimina).
        // - elementos: El array destino (el mismo array 'elementos' en este caso).
        // - indice: La posición inicial en el array destino donde se copian los elementos (la posición del elemento que se elimina).
        // - size - indice - 1: El número de elementos que se van a copiar.  Calculamos cuántos elementos hay que desplazar.
        System.arraycopy(elementos, indice + 1, elementos, indice, size - indice - 1);

        // 3. Decremento del tamaño:
        // Decrementamos el tamaño de la lista, ya que hemos eliminado un elemento.
        size--;

        // 4.  "Limpieza" del último elemento (opcional, pero buena práctica):
        //  Ponemos a null el último elemento del array (el que ha quedado "duplicado" al desplazar los elementos).
        //  Esto ayuda al Garbage Collector a liberar la memoria ocupada por ese objeto, especialmente si son objetos grandes.
        elementos[size] = null; // size ahora apunta al "hueco" que dejamos.
    }

Ventaja: Aumenta la velocidad de eliminación, especialmente en listas grandes.


5. Implementar la Interfaz List

Problema: No sigue los estándares de Java.
Solución: Hacer que ListaArray implemente la interfaz List.

Ventaja: Permite usar ListaArray con métodos y estructuras que esperan una lista estándar.


Ejemplo Completo con Mejoras

Código:

public class Main {
    public static void main(String[] args) {
        ListaArray<String> lista = new ListaArray<>();

        lista.add("Manzana");
        lista.add("Banana");
        lista.add("Cereza");

        System.out.println("Elemento en índice 1: " + lista.get(1));
        System.out.println("¿Contiene 'Banana'? " + lista.contains("Banana"));

        lista.remove(1);
        System.out.println("¿Contiene 'Banana' después de eliminar? " + lista.contains("Banana"));

        lista.clear();
        System.out.println("Tamaño después de limpiar: " + lista.size());
    }
}

Salida:

Elemento en índice 1: Banana
¿Contiene 'Banana'? true
¿Contiene 'Banana' después de eliminar? false
Tamaño después de limpiar: 0


Conclusión

Estas mejoras convierten a ListaArray en una estructura más versátil, eficiente y alineada con las necesidades reales de programación. Ahora puedes usarla para manejar cualquier tipo de datos, optimizar su rendimiento y aprovechar métodos comunes. Estas prácticas te acercan a escribir código profesional y preparado para aplicaciones reales. 🚀

¿Listo para el desafío? Intenta implementar estas mejoras en tu propia clase ListaArray y comprueba cómo evoluciona tu código. 💻


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.