Las colecciones son la parte de la biblioteca estándar que se usa todos los días. Esta lección va de las tres que importan, de por qué una clave mal hecha hace desaparecer datos de un Map, y de un error al borrar que unas veces avisa y otras no.
Tres formas de guardar varias cosas
| Admite repetidos | Orden | Buscar | |
|---|---|---|---|
List |
sí | el de inserción | recorriendo: lento |
Set |
no | ninguno (en HashSet) |
instantáneo |
Map |
claves no, valores sí | ninguno (en HashMap) |
instantáneo por clave |
Esos tres nombres son interfaces. Lo que se crea con new es una implementación concreta, y lo idiomático es declarar por la interfaz:
List<String> compra = new ArrayList<>(); Set<String> unicos = new HashSet<>(); Map<String,Integer> stock = new HashMap<>();
List
salida[pan, uva, pan] size=3 get(1)=uva
contains(uva)=true indexOf(pan)=0
tras remove("pan"): [uva, pan]
Al contrario que un array, crece sola. Y ojo con remove: quita la primera aparición, no todas. Otra trampa clásica es que remove(int) y remove(Object) son métodos distintos: en una List<Integer>, remove(2) borra la posición 2, y remove(Integer.valueOf(2)) borra el valor 2.
Set
salidaHashSet [uva, pan] LinkedHashSet [c, a, b] TreeSet (ordena) [a, b, c]
Las tres variantes sirven para lo mismo y se diferencian en el orden: HashSet no garantiza ninguno, LinkedHashSet conserva el de inserción y TreeSet ordena. Si el orden importa, elígelo a propósito; no confíes en el que salga.
Map
stock.put("pan", 3);
stock.put("pan", 5); // sustituye, no añade
IO.println(stock.getOrDefault("kiwi", 0));
stock.merge("pan", 2, Integer::sum); // suma 2 a lo que haya, o pone 2 si no hay nada
stock.computeIfAbsent("kiwi", k -> 1);
for (var e : stock.entrySet()) IO.println(e.getKey() + " -> " + e.getValue());
salida{uva=10, pan=5}
get(pan)=5 get(kiwi)=null
getOrDefault(kiwi,0)=0
tras merge(+2): 7
computeIfAbsent: {uva=10, kiwi=1, pan=7}
get de una clave que no existe devuelve null, y de ahí salen la mitad de los NullPointerException del mundo. getOrDefault, merge y computeIfAbsent existen justo para no tener que escribir el «si no está, pon esto» a mano, y hacen el código bastante más corto.
Por qué un Map necesita equals y hashCode
Este experimento explica de golpe por qué la lección anterior insistía tanto:
record Clave(String a) { } // equals y hashCode hechos
class Mala { final String a; Mala(String a){this.a=a;} } // sin equals ni hashCode
salidacon record: valor sin equals: null
Mismo contenido, misma operación, resultado distinto. Un HashMap busca la clave calculando su hashCode y comparando con equals; si la clase no los define, usa los heredados, que comparan identidad. Resultado: metes algo y no lo vuelves a encontrar nunca.
Map o elemento de un Set necesita equals y hashCode. En la práctica eso significa: usa un record, o un String, o un enum.La trampa de borrar mientras se recorre
Lo que todo el mundo intenta alguna vez:
for (String s : lista) if (s.equals(quitar)) lista.remove(s);
Lo probé quitando cada uno de los tres elementos de [a, b, c]:
salidaquitando "a": ConcurrentModificationException quitando "b": NO salta nada, queda [a, c] quitando "c": ConcurrentModificationException
La forma correcta es una línea:
lista.removeIf(s -> s.equals("b"));
salidacon removeIf: [a, c]
Ordenar
ps.sort(Comparator.comparingDouble(P::precio)); ps.sort(Comparator.comparing(P::n).reversed()); Collections.max(ps, Comparator.comparingDouble(P::precio));
salidapor precio: [P[n=bufanda, precio=9.0], P[n=gorra, precio=12.5], P[n=mochila, precio=34.0]] por nombre, al revés: [P[n=mochila, precio=34.0], P[n=gorra, precio=12.5], P[n=bufanda, precio=9.0]] el más caro: P[n=mochila, precio=34.0]
Comparator se construye por composición: comparing para extraer el campo, reversed() para invertir y thenComparing para el criterio de desempate. Esa sintaxis con :: es una referencia a método, y es lo que viene en la lección 9.
Genéricos
Los corchetes angulares de List<String> son el motivo de que no haga falta ningún cast al sacar elementos, y de que esto no compile:
List<String> solo = new ArrayList<>(); solo.add(42); // error: incompatible types: int cannot be converted to String
También se pueden escribir métodos genéricos propios. La <T> delante del tipo de retorno declara el parámetro de tipo:
<T> T primero(List<T> l) { return l.isEmpty() ? null : l.get(0); }
List<String> es solo una List. Por eso no se puede preguntar si algo es una List<String>, ni crear un new T[]. Es el precio que se pagó por no romper el código anterior a 2004.Lo esencial
| Quieres… | Usa |
|---|---|
| Una lista que crece | List<T> x = new ArrayList<>() |
| Sin repetidos | Set<T>; LinkedHashSet si el orden importa |
| Buscar por clave | Map<K,V> |
Evitar el null de get |
getOrDefault, merge, computeIfAbsent |
| Borrar según una condición | removeIf, nunca dentro de un for-each |
| Que no se pueda modificar | List.of, Set.of, Map.of |
| Ordenar por un campo | Comparator.comparing(T::campo) |
Todo el código de esta lección se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicarla; las salidas y los errores del compilador están copiados de esa ejecución.