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
- Falta de Flexibilidad para Otros Tipos de Datos:
- Actualmente,
ListaArray
solo funciona conint
. Esto limita su uso, ya que no puedes almacenar otros tipos de datos comoString
,double
o incluso objetos.
- Actualmente,
- Capacidad Inicial Fija:
- Aunque la capacidad crece dinámicamente, comienza siempre en 10, lo que puede no ser ideal para todas las aplicaciones.
- 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.
- No incluye métodos comunes como:
- Eliminación Ineficiente:
- El método
remove
mueve manualmente los elementos después del índice eliminado. Esto puede ser lento si hay muchos elementos.
- El método
- 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.
- Actualmente, usamos
- No Implementa Interfaces Estándar:
ListaArray
no implementa ninguna interfaz comoList
, 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