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