Elige bien tu colección

Si llevas un tiempo programando en Java, seguramente ya has sufrido la limitación más frustrante de los arrays: una vez que los creas, su tamaño está grabado en piedra. ¿Necesitas añadir un elemento más? Mala suerte. ¿No sabes de antemano cuántos elementos vas a tener? Peor todavía.

Java resuelve este problema con las colecciones: estructuras de datos dinámicas, flexibles y bien diseñadas que vienen de serie en el paquete java.util. Hay varios tipos, y elegir el adecuado para cada situación marca la diferencia entre un código claro y eficiente y uno que te da más problemas de los que resuelve. En este artículo repasamos los tres tipos fundamentales y te damos las claves para elegir bien.


List (Listas): el array de toda la vida, pero mejor

Una List es una colección ordenada (cada elemento tiene su posición) que admite repeticiones. Es lo más parecido a un array, con la diferencia de que crece y se encoge según necesites.

La implementación que usarás el 90% de las veces es ArrayList:

List<String> compra = new ArrayList<>();
compra.add("leche");
compra.add("pan");
compra.add("pan");       // sí, repetido; una List lo permite
compra.add("huevos");

System.out.println(compra.size());    // 4
System.out.println(compra.get(1));    // "pan"
compra.remove("pan");                 // elimina la primera ocurrencia
System.out.println(compra);          // [leche, pan, huevos]

El detalle de Integer en lugar de int

Las colecciones solo pueden almacenar objetos, no tipos primitivos. Esto significa que no puedes hacer List<int>. Tienes que usar la clase envoltorio correspondiente: Integer para int, Double para double, Boolean para boolean, etc.

List<Integer> notas = new ArrayList<>();
notas.add(7);    // Java convierte automáticamente int → Integer
notas.add(9);
notas.add(6);

int primera = notas.get(0);   // y también Integer → int al sacarlo

Esta conversión automática se llama autoboxing y Java la hace en silencio, así que en la práctica casi no se nota. Lo importante es recordar que en la declaración del tipo hay que escribir Integer, no int.

¿Cuándo usar List?

Usa una List cuando el orden importe, cuando necesites acceder por posición o cuando quieras permitir repeticiones. Lista de tareas, historial de acciones, colección de notas de un alumno: todo eso es territorio List.


Obtener una lista a partir de un array

String[] arrayNormal = {"Primera", "Segunda", "Tercera"};  // Array normal
System.out.println(Arrays.toString(arrayNormal)); // Imprimir un array normal: utilidad Arrays.toString()

List<String> listaInmutable = Arrays.asList(arrayNormal); // Inmutable: no puedo añadir ni borrar elementos
System.out.println(listaInmutable); // Imprimir directamente

List<String> listaNormal = new ArrayList<>(Arrays.asList(arrayNormal)); // ArrayList normal
System.out.println(listaNormal); // Imprimir una lista: directamente

Recorrer una colección: for-each e Iterator

La forma más sencilla de recorrer cualquier colección es el bucle for-each:

List<String> compra = new ArrayList<>(Arrays.asList("leche", "pan", "huevos"));

for (String producto : compra) {
   System.out.println(producto);
}

Sin embargo, el for-each tiene una limitación importante: no se pueden añadir ni eliminar elementos durante el recorrido. Si lo intentas, Java lanza una ConcurrentModificationException en tiempo de ejecución. Cuando necesites eliminar elementos mientras recorres la colección, tienes que usar un Iterator:

List<String> compra = new ArrayList<>(Arrays.asList("leche", "pan", "huevos", "pan"));

Iterator<String> iter = compra.iterator(); // El iterador antes del primer elemento de la colección

while (iter.hasNext()) {
   String producto = iter.next();
   if (producto.equals("pan")) {
       iter.remove();   // eliminación segura
  }
}

System.out.println(compra);  // [leche, huevos]

El iterador mantiene un cursor interno que va avanzando elemento a elemento. Su método remove() está coordinado con la colección, por lo que la eliminación es siempre segura. ¡Pero sigues sin poder añadir!

Los métodos de Iterator:

  • hasNext()
  • next()
  • remove()

ListIterator: recorrer una lista al revés

Las listas ofrecen un iterador más potente, ListIterator: es capaz de recorrer la colección en los dos sentidos, eliminar elementos durante el recorrido y permite también modificarlos con set() e incluso insertar nuevos con add().

Para recorrer una lista de atrás hacia adelante, se posiciona el cursor al final y se usa hasPrevious() y previous():

List<String> palabras = new ArrayList<>(Arrays.asList("uno", "dos", "tres", "cuatro"));

ListIterator<String> iter = palabras.listIterator(palabras.size()); // Iterador después del último
while (iter.hasPrevious()) {
   System.out.print(iter.previous() + " ");
}
// Salida: cuatro tres dos uno

listIterator(palabras.size()) sitúa el cursor justo después del último elemento, listo para empezar a retroceder. Es el equivalente a posicionarse al final de la cinta antes de darle al rebobinar.

Los métodos de ListIterator:

  • hasNext()
  • next()
  • hasPrevious()
  • previous()
  • add(), set(), remove()

En resumen:

Operación durante el recorridofor-eachIteratorListIterator
Leer elementos
Modificar estado interno del elemento
Sustituir el elemento en la colección✅ (set())
Eliminar elemento actual
Añadir elementos✅ (add())

Set (Conjuntos): cuando los repetidos son el enemigo

Un Set implementa el concepto matemático de conjunto: no admite elementos repetidos y, en su versión más común (HashSet), no garantiza ningún orden particular.

Set<String> etiquetas = new HashSet<>();
etiquetas.add("java");
etiquetas.add("programación");
etiquetas.add("java");       // ignorado: ya existe
etiquetas.add("colecciones");

System.out.println(etiquetas.size());  // 3, no 4

El truco del siglo: eliminar duplicados de una lista

Una de las operaciones más frecuentes en programación es eliminar los elementos repetidos de una lista. Con un Set esto se hace en una sola línea:

String[] arrayNormal = {"rosa", "tulipán", "rosa", "margarita", "tulipán", "rosa"};

List<String> listaInmutable = new ArrayList<>(Arrays.asList(arrayNormal));

Set<String> cjtoSinRepetidos = new HashSet<>(listaInmutable);
System.out.println(cjtoSinRepetidos);  // [rosa, tulipán, margarita] (en algún orden)

List<String> listaSinRepetidos = new ArrayList<>(cjtoSinRepetidos); // Lista sin repetidos

El constructor de HashSet acepta cualquier colección como argumento y, al añadir los elementos uno a uno, descarta automáticamente los que ya están. No garantiza ningún orden concreto. Si además quieres que el resultado esté ordenado:

  • Orden natural: alfabéticamente, de menor a mayor -> usa TreeSet
  • Orden de inserción -> usa LinkedHashSet

El peaje: equals() y hashCode()

Aquí viene la parte que a mucha gente le pilla por sorpresa. El Set funciona de maravilla con String e Integer porque Java ya sabe cómo comparar esos tipos. Pero si intentas usar un Set con objetos de tus propias clases, tienes que enseñarle a Java qué significa que dos objetos sean “iguales”.

Imagina que tienes una clase Cancion:

public class Cancion {
   private String titulo;
   private String artista;

   public Cancion(String titulo, String artista) {
       this.titulo = titulo;
       this.artista = artista;
  }
}

Si haces esto:

Set<Cancion> favoritas = new HashSet<>();
favoritas.add(new Cancion("Bohemian Rhapsody", "Queen"));
favoritas.add(new Cancion("Bohemian Rhapsody", "Queen"));  // ¿duplicado?

System.out.println(favoritas.size());  // 2 😱

El conjunto contiene dos elementos, cuando esperabas uno. ¿Por qué? Porque Java, por defecto, compara objetos por su referencia en memoria, no por su contenido. Las dos canciones son objetos distintos aunque tengan los mismos datos.

La solución es sobrescribir dos métodos en tu clase: equals(), que define cuándo dos objetos son iguales, y hashCode(), que genera un código numérico usado internamente por el HashSet para localizar elementos rápidamente. La regla de oro es que si dos objetos son iguales según equals(), deben producir el mismo hashCode().

@Override
public boolean equals(Object o) {
   if (!(o instanceof Cancion)) {
       return false;
  }
   Cancion otra = (Cancion) o;
   return titulo.equals(otra.titulo) && artista.equals(otra.artista);
}

@Override
public int hashCode() {
   return Objects.hash(titulo, artista);
}

Objects.hash() es un método utilitario de Java que calcula un hashCode razonable a partir de los atributos que le pases. Úsalo siempre: es sencillo, correcto y evita errores.

Con estos dos métodos añadidos a Cancion, el conjunto ya funciona como esperas:

favoritas.add(new Cancion("Bohemian Rhapsody", "Queen"));
favoritas.add(new Cancion("Bohemian Rhapsody", "Queen"));

System.out.println(favoritas.size());  // 1 ✅

Método equals() alternativo

El método equals() resuelto con instanceof, da por válida cualquier clase derivada. Si quieres limitar a la clase concreta, es mejor usar la siguiente versión (¡Pero te obliga a comprobar que o no es null!):

@Override
public boolean equals(Object o) {
   if ( (o==null) || (o.getClass()!=this.getClass()) ) {
       return false;
  }
   Cancion otra = (Cancion) o;
   return titulo.equals(otra.titulo) && artista.equals(otra.artista);
}

¿Cuándo usar Set?

Usa un Set cuando los duplicados no tengan sentido en tu problema: etiquetas, identificadores únicos, palabras distintas en un texto. Y recuerda sobreescribir equals() y hashCode() si los elementos son de tus propias clases.

Map (Mapas): cada cosa tiene su etiqueta

Un Map almacena pares clave → valor. Las claves no se pueden repetir; cada clave tiene exactamente un valor asociado. Es la estructura perfecta para representar relaciones: nombre → teléfono, producto → precio, palabra → número de apariciones.

Map<String, Integer> puntuaciones = new HashMap<>();
puntuaciones.put("Ana", 10);
puntuaciones.put("Luis", 7);
puntuaciones.put("Eva", 9);

System.out.println(puntuaciones.get("Ana"));    // 10
System.out.println(puntuaciones.get("Mario"));  // null: no existe

puntuaciones.put("Ana", 8);  // reemplaza el valor anterior
System.out.println(puntuaciones.get("Ana"));    // 8

Un ejemplo muy típico es contar cuántas veces aparece cada palabra en un texto:

String[] palabras = {"java", "es", "genial", "java", "mola", "java"};
Map<String, Integer> conteo = new HashMap<>();

for (String palabra : palabras) {
   if (conteo.containsKey(palabra)) {
       conteo.put(palabra, conteo.get(palabra) + 1);
  } else {
       conteo.put(palabra, 1);
  }
}

System.out.println(conteo);  // {java=3, es=1, genial=1, mola=1}

Para recorrer un mapa necesitas hacerlo a través de sus claves, sus valores, o ambos a la vez:

// Solo claves
for (String nombre : puntuaciones.keySet()) {
   System.out.println(nombre + " → " + puntuaciones.get(nombre));
}

// Clave y valor a la vez (más eficiente)
for (Map.Entry<String, Integer> entrada : puntuaciones.entrySet()) {
   System.out.println(entrada.getKey() + " → " + entrada.getValue());
}

Para hacer operaciones sobre Maps, es habitual obtener el conjunto de las claves con el método keySet():

Set<String> nombres = puntuaciones.keySet();

A partir del conjunto de las claves, podríamos también obtener un ArrayList con las claves, que nos puede servir para recorrerlas ordenadamente:

List<String> listaNombres = new ArrayList<>(puntuaciones.keySet());

En las colecciones, como List o Set, se puede pasar al constructor otra colección como argumento. Sería equivalente a utilizar el método addAll():

List<String> listaNombres = new ArrayList<>();
listaNombres.addAll(puntuaciones.keySet());

¿Cuándo usar Map?

Usa un Map cuando tengas una relación clave → valor y necesites buscar por la clave. Si te encuentras usando dos listas en paralelo (una de nombres y otra de valores) para mantener esa correspondencia, es una señal clara de que lo que necesitas es un Map.


Dos clases que te harán la vida más fácil

Arrays: el puente entre arrays y colecciones

La clase Arrays (plural) ofrece métodos estáticos para operar sobre arrays. Los más útiles:

int[] nums = {5, 3, 8, 1, 9};

// Imprimir un array (sin esto obtendrías algo como [I@4e50df2u)
System.out.println(Arrays.toString(nums));       // [5, 3, 8, 1, 9]

// Ordenar
Arrays.sort(nums);
System.out.println(Arrays.toString(nums));       // [1, 3, 5, 8, 9]

// Buscar (solo en arrays ya ordenados)
int pos = Arrays.binarySearch(nums, 8);         // pos = 3

// Convertir array a lista (para pasarlo a una colección)
List<String> lista = new ArrayList<>(Arrays.asList("a", "e", "i", "o", "u"));

Ese último patrón, new ArrayList<>(Arrays.asList(...)), es uno de los más usados en Java para crear una lista con valores iniciales de forma compacta.

Collections: utilidades para colecciones

La clase Collections (también plural, y fácil de confundir con el interface Collection en singular) ofrece métodos estáticos para operar sobre colecciones:

List<Integer> nums = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5, 9));

Collections.sort(nums);     // [1, 1, 3, 4, 5, 9]
Collections.reverse(nums);  // [9, 5, 4, 3, 1, 1]
Collections.shuffle(nums);  // orden aleatorio (útil para juegos, tests...)

System.out.println(Collections.min(nums));         // mínimo
System.out.println(Collections.max(nums));         // máximo
System.out.println(Collections.frequency(nums, 1)); // cuántas veces aparece el 1

Resumen: ¿cuál uso?

SituaciónColección recomendada
Necesito orden y puedo tener repetidosListArrayList
No quiero repetidos y el orden no importaSetHashSet
No quiero repetidos y quiero orden de inserciónSetLinkedHashSet
No quiero repetidos y quiero orden alfabéticoSetTreeSet
Necesito asociar claves a valoresMapHashMap
Como el anterior pero en orden de inserciónMapLinkedHashMap
Como el anterior pero ordenado por claveMapTreeMap

Y una última regla que resume mucho: declara siempre la variable con el tipo del interface (List, Set, Map), no con el de la clase concreta (ArrayList, HashSet, HashMap). Así, si un día decides cambiar de implementación, solo tendrás que tocar una línea de código.

// Así no
ArrayList<String> lista = new ArrayList<>();

// Así sí
List<String> lista = new ArrayList<>();

Las colecciones de Java son una de esas partes del lenguaje que, una vez que las conoces bien, no entiendes cómo vivías sin ellas. ¡Elige bien y que el código fluya!

Deja una respuesta

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