Módulo 16: Colecciones y genéricos
Objetivos del módulo
- Conocer las limitaciones de los arrays y el framework de colecciones de Java
- Entender los genéricos (
<T>), el operador diamante y por qué evitan los casts - Trabajar con listas (
ArrayList,LinkedList): operaciones, recorridos, iteradores y borrado seguro - Usar conjuntos (
HashSet,LinkedHashSet,TreeSet) y entender la importancia deequals()yhashCode() - Usar mapas (
HashMap,LinkedHashMap,TreeMap), recorrerlos y contar frecuencias - Implementar pilas y colas con
ArrayDequey colas de prioridad conPriorityQueue - Ordenar con
ComparableyComparator(comparing,thenComparing,reversed) - Aprovechar la clase
Collections, las colecciones inmutables (List.of…) y las colecciones secuenciadas de Java 21 - Crear clases y métodos genéricos propios
- Elegir la colección adecuada para cada problema
1. Por qué necesitamos colecciones
En el módulo 08 trabajaste con arrays. Son rápidos y sencillos, pero tienen un tamaño fijo (si no sabes cuántos datos habrá, tienes que reservar “de más” o crear un array mayor y copiar), insertar o borrar en medio te obliga a desplazar elementos a mano, y no ofrecen operaciones como “¿contiene?”, “borrar este valor” o “buscar por clave”. Compara:
String[] nombres = new String[2]; // tamaño decidido de antemano
nombres = Arrays.copyOf(nombres, 3); // "crecer" = crear otro array y copiar
List<String> lista = new ArrayList<>(); // crece sola
lista.add("Ana");
lista.add("Eva");
lista.add(1, "Marta"); // insertar en medio: desplaza al resto -> [Ana, Marta, Eva]
lista.remove("Ana"); // borrar por valor -> [Marta, Eva]
📘 Concepto: Una colección es un objeto que agrupa otros objetos y ofrece operaciones para añadirlos, buscarlos, borrarlos y recorrerlos, creciendo y encogiendo automáticamente. Java incluye un conjunto completo de colecciones en el paquete
java.util, conocido como el Java Collections Framework.
1.1. El framework de colecciones
El framework se organiza en interfaces (qué se puede hacer) y clases que las implementan (cómo se hace por dentro):
Figura 1. Las interfaces y clases principales del framework de colecciones.
| Interfaz | Qué representa | Admite duplicados | Implementaciones habituales |
|---|---|---|---|
List<E> | Secuencia ordenada por posición (índice 0, 1, 2…) | Sí | ArrayList, LinkedList |
Set<E> | Conjunto de elementos únicos | No | HashSet, LinkedHashSet, TreeSet |
Queue<E> / Deque<E> | Cola (y cola doble: por los dos extremos) | Sí | ArrayDeque, PriorityQueue, LinkedList |
Map<K, V> | Parejas clave → valor, con claves únicas | Claves no; valores sí | HashMap, LinkedHashMap, TreeMap |
💡 Consejo: Declara las variables con el tipo de la interfaz y crea el objeto con la clase:
List<String> nombres = new ArrayList<>();. Es polimorfismo (módulo 13): si mañana quieres cambiar aLinkedList, solo cambias una palabra.
2. Genéricos: colecciones con tipo
2.1. El problema sin genéricos
Antes de Java 5 las colecciones guardaban Object. Se podía meter cualquier cosa y, al sacarla, había que hacer un cast… que podía fallar en tiempo de ejecución:
import java.util.ArrayList;
public class SinGenericos {
public static void main(String[] args) {
ArrayList lista = new ArrayList(); // "tipo crudo": sin <...>
lista.add("hola");
lista.add(42); // el compilador no protesta
for (Object o : lista) {
String s = (String) o; // cast obligatorio... y peligroso
System.out.println(s.toUpperCase());
}
}
}
Salida (los avisos del compilador, resumidos, y la ejecución):
SinGenericos.java:6: warning: [unchecked] unchecked call to add(E) as a member of the raw type ArrayList
SinGenericos.java:7: warning: [unchecked] unchecked call to add(E) as a member of the raw type ArrayList
2 warnings
HOLA
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at SinGenericos.main(SinGenericos.java:9)
El compilador solo da avisos (warnings), el programa se ejecuta y el error aparece al ejecutar, en la segunda vuelta del bucle.
2.2. La solución: parámetros de tipo
📘 Concepto: Los genéricos permiten indicar entre
< >el tipo de los elementos de una colección:List<String>es “una lista deString”. El compilador comprueba que solo metesStringy, al sacar un elemento, ya sabe que es unString: no hace falta cast y los errores se detectan al compilar.
List<String> nombres = new ArrayList<>(); // el <> vacío es el "operador diamante" (Java 7)
nombres.add("Ana");
nombres.add(42); // ERROR de compilación: 42 no es un String
String primero = nombres.get(0); // sin cast
| Elemento | Significado |
|---|---|
List<String> | Tipo parametrizado: lista de String |
new ArrayList<>() | Diamante: el compilador deduce <String> de la izquierda |
Map<String, Integer> | Dos parámetros: tipo de la clave y tipo del valor |
List<int> | No válido: los genéricos solo admiten objetos. Usa la clase envoltorio List<Integer> (módulo 11); el autoboxing convierte int ↔ Integer automáticamente |
3. Listas: ArrayList y LinkedList
3.1. Operaciones básicas y recorridos
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListaBasica {
public static void main(String[] args) {
List<String> compra = new ArrayList<>(); // interfaz a la izquierda, clase a la derecha
compra.add("pan");
compra.add("leche");
compra.add("huevos");
compra.add(0, "café"); // insertar en una posición
compra.add("leche"); // las listas admiten duplicados
System.out.println("Lista: " + compra);
System.out.println("Tamaño: " + compra.size());
System.out.println("Elemento 2: " + compra.get(2));
System.out.println("¿Hay huevos? " + compra.contains("huevos"));
System.out.println("Primera leche en: " + compra.indexOf("leche")
+ ", última en: " + compra.lastIndexOf("leche"));
compra.set(1, "pan integral"); // reemplazar
compra.remove(compra.size() - 1); // borrar por posición
compra.remove("café"); // borrar por valor
System.out.println("Después de cambios: " + compra);
// Tres formas de recorrer
for (int i = 0; i < compra.size(); i++) { // 1) con índice
System.out.print(i + "=" + compra.get(i) + " ");
}
System.out.println();
for (String producto : compra) { // 2) for-each (la más habitual)
System.out.print(producto.toUpperCase() + " ");
}
System.out.println();
Iterator<String> it = compra.iterator(); // 3) iterador explícito
while (it.hasNext()) {
System.out.print("[" + it.next() + "] ");
}
System.out.println();
compra.clear();
System.out.println("¿Vacía? " + compra.isEmpty());
}
}
Salida:
Lista: [café, pan, leche, huevos, leche]
Tamaño: 5
Elemento 2: leche
¿Hay huevos? true
Primera leche en: 2, última en: 4
Después de cambios: [pan integral, leche, huevos]
0=pan integral 1=leche 2=huevos
PAN INTEGRAL LECHE HUEVOS
[pan integral] [leche] [huevos]
¿Vacía? true
| Método | Qué hace |
|---|---|
add(e) / add(i, e) | Añade al final / en la posición i (desplaza el resto) |
get(i) / set(i, e) | Lee / reemplaza la posición i |
remove(i) / remove(objeto) | Borra por posición / la primera aparición del objeto (usa equals) |
size(), isEmpty(), clear() | Tamaño, ¿vacía?, vaciar |
contains(e), indexOf(e), lastIndexOf(e) | Buscar (usan equals); indexOf devuelve -1 si no está |
addAll(otra), subList(desde, hasta) | Añadir todos los de otra colección; vista de un tramo |
⚠️ Importante: Con
List<Integer>,remove(1)borra la posición 1, no el valor 1. Para borrar el valor, conviértelo en objeto:
List<Integer> numeros = new ArrayList<>(List.of(10, 20, 30, 1));
numeros.remove(1); // remove(int índice): borra el 20 -> [10, 30, 1]
numeros.remove(Integer.valueOf(1)); // remove(Object): borra el valor 1 -> [10, 30]
3.2. Eliminar elementos mientras se recorre
Borrar elementos dentro de un for-each es un error clásico: la lista cambia “por debajo” del recorrido y Java lanza ConcurrentModificationException. Hay dos formas correctas:
📘 Concepto: Una lambda es una función breve escrita en el sitio donde se usa:
n -> n > 4significa “dadon, devuelve sin > 4”. Muchos métodos de las colecciones reciben lambdas. Una referencia a método comoAlumno::getNotaes una forma abreviada de la lambdaa -> a.getNota(). Las estudiarás a fondo en el Curso Avanzado; aquí basta con leerlas así.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class EliminarRecorrido {
public static void main(String[] args) {
List<Integer> numeros = new ArrayList<>(List.of(3, 8, 5, 12, 7, 4));
// Forma 1: Iterator.remove()
Iterator<Integer> it = numeros.iterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) {
it.remove(); // borra el último elemento devuelto por next()
}
}
System.out.println("Sin pares: " + numeros);
// Forma 2: removeIf con una condición (lambda)
numeros.removeIf(n -> n > 4);
System.out.println("Sin mayores que 4: " + numeros);
// Lo que NO se debe hacer: borrar dentro de un for-each
List<String> nombres = new ArrayList<>(List.of("Ana", "Bea", "Carlos", "Dani"));
for (String n : nombres) {
if (n.startsWith("B")) {
nombres.remove(n);
}
}
}
}
Salida:
Sin pares: [3, 5, 7]
Sin mayores que 4: [3]
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
at EliminarRecorrido.main(EliminarRecorrido.java:24)
removeIf es la forma más corta y clara; el Iterator es útil cuando la lógica es más compleja o haces otras cosas en el mismo recorrido.
3.3. LinkedList
LinkedList implementa List y también Deque, así que, además de todo lo anterior, añade operaciones por los extremos:
LinkedList<String> tareas = new LinkedList<>(List.of("estudiar"));
tareas.addFirst("desayunar"); // [desayunar, estudiar]
tareas.addLast("cenar"); // [desayunar, estudiar, cenar]
tareas.getFirst(); // "desayunar"
tareas.removeFirst(); // [estudiar, cenar]
3.4. ¿ArrayList o LinkedList?
Por dentro funcionan de forma muy distinta:
Figura 2. Estructura interna de ArrayList y de LinkedList.
Para comparar su rendimiento se usa la notación O grande: O(1) significa “tiempo constante, da igual el tamaño”; O(n), “proporcional al número de elementos”; O(log n), “crece muy despacio” (duplicar los datos solo añade un paso).
| Operación | ArrayList | LinkedList |
|---|---|---|
get(i) / set(i, e) | O(1) | O(n): recorre nodos |
add(e) al final | O(1) (casi siempre) | O(1) |
add(0, e) / remove(0) (al principio) | O(n): desplaza todo | O(1) |
add(i, e) / remove(i) en medio | O(n): desplaza | O(n): primero hay que llegar |
contains(e) / indexOf(e) | O(n) | O(n) |
| Memoria | Compacta | Un nodo con dos referencias extra por elemento |
💡 Consejo: Usa
ArrayListpor defecto. Gracias a que sus datos están juntos en memoria, en la práctica es más rápida queLinkedListincluso en muchas operaciones donde la teoría dice lo contrario. Si necesitas trabajar por los extremos (pila o cola), usaArrayDeque(sección 6).
4. Conjuntos: Set
Un Set no admite duplicados: si añades un elemento que ya está, add devuelve false y no hace nada. Es ideal para quitar repetidos o para comprobar rápidamente si algo pertenece a un grupo.
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
public class Conjuntos {
public static void main(String[] args) {
String[] palabras = {"pera", "manzana", "kiwi", "pera", "uva", "kiwi", "fresa"};
Set<String> hash = new HashSet<>();
Set<String> enlazado = new LinkedHashSet<>();
Set<String> arbol = new TreeSet<>();
for (String p : palabras) {
hash.add(p);
enlazado.add(p);
arbol.add(p);
}
System.out.println("HashSet: " + hash); // orden no garantizado
System.out.println("LinkedHashSet: " + enlazado); // orden de inserción
System.out.println("TreeSet: " + arbol); // orden natural (alfabético)
System.out.println("Distintas: " + arbol.size() + " de " + palabras.length);
System.out.println("add(\"uva\") otra vez: " + arbol.add("uva")); // false: ya estaba
// Operaciones de conjuntos (sobre copias, para no modificar los originales)
Set<String> a = new TreeSet<>(Set.of("pera", "uva", "kiwi"));
Set<String> b = new TreeSet<>(Set.of("kiwi", "fresa", "pera"));
Set<String> union = new TreeSet<>(a);
union.addAll(b);
Set<String> interseccion = new TreeSet<>(a);
interseccion.retainAll(b);
Set<String> diferencia = new TreeSet<>(a);
diferencia.removeAll(b);
System.out.println("A ∪ B = " + union + ", A ∩ B = " + interseccion + ", A - B = " + diferencia);
}
}
Salida:
HashSet: [pera, manzana, uva, kiwi, fresa]
LinkedHashSet: [pera, manzana, kiwi, uva, fresa]
TreeSet: [fresa, kiwi, manzana, pera, uva]
Distintas: 5 de 7
add("uva") otra vez: false
A ∪ B = [fresa, kiwi, pera, uva], A ∩ B = [kiwi, pera], A - B = [uva]
| Clase | Orden al recorrer | add / contains | Úsala cuando… |
|---|---|---|---|
HashSet | Sin orden garantizado (puede cambiar) | O(1) | Solo te importa “está o no está” |
LinkedHashSet | Orden de inserción | O(1) | Quieres quitar duplicados conservando el orden |
TreeSet | Ordenado (natural o con Comparator) | O(log n) | Necesitas los elementos ordenados, el menor, el mayor… |
⚠️ Importante: No escribas programas que dependan del orden de un
HashSeto unHashMap: no está garantizado y puede cambiar entre versiones de Java o al añadir elementos.
4.1. La importancia de equals() y hashCode()
¿Cómo sabe un HashSet que dos objetos son “el mismo”? Usa hashCode() para decidir en qué zona de su tabla interna buscar y equals() para comparar dentro de esa zona:
Figura 3. Esquema de la tabla hash interna de HashSet y HashMap.
Si tu clase no sobrescribe equals() y hashCode(), Java usa los de Object, que comparan identidad (si es el mismo objeto en memoria). Resultado: dos objetos con los mismos datos se consideran distintos.
import java.util.HashSet;
import java.util.Set;
class PersonaSinEquals {
private final String dni;
PersonaSinEquals(String dni) { this.dni = dni; }
@Override public String toString() { return dni; }
}
record Persona(String dni) { } // equals y hashCode generados
public class EqualsHashCode {
public static void main(String[] args) {
Set<PersonaSinEquals> mal = new HashSet<>();
mal.add(new PersonaSinEquals("123A"));
mal.add(new PersonaSinEquals("123A"));
System.out.println("Sin equals/hashCode: " + mal.size() + " elementos " + mal);
Set<Persona> bien = new HashSet<>();
bien.add(new Persona("123A"));
bien.add(new Persona("123A"));
System.out.println("Con equals/hashCode: " + bien.size() + " elemento " + bien);
System.out.println("¿Contiene 123A? " + bien.contains(new Persona("123A")));
}
}
Salida:
Sin equals/hashCode: 2 elementos [123A, 123A]
Con equals/hashCode: 1 elemento [Persona[dni=123A]]
¿Contiene 123A? true
⚠️ Importante: Cualquier clase que vayas a guardar en un
HashSeto usar como clave de unHashMapdebe sobrescribirequals()yhashCode()de forma coherente (módulo 12), o ser un record, que ya los trae. ParaTreeSetyTreeMaplo que cuenta escompareTo()o elComparator(sección 7). Y no modifiques un objeto mientras está dentro de unHashSet: suhashCodecambiaría y el conjunto ya no lo encontraría.
5. Mapas: Map
Un mapa asocia claves únicas con valores, como un diccionario (palabra → definición) o una agenda (nombre → teléfono). Buscar por clave es inmediato, sin recorrer nada.
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class Mapas {
public static void main(String[] args) {
Map<String, Integer> edades = new HashMap<>();
edades.put("Lucía", 31);
edades.put("Andrés", 25);
edades.put("Marta", 40);
Integer anterior = edades.put("Andrés", 26); // misma clave: reemplaza
System.out.println("Valor anterior de Andrés: " + anterior);
System.out.println("Edad de Marta: " + edades.get("Marta"));
System.out.println("Edad de Pablo: " + edades.get("Pablo")); // null
System.out.println("Edad de Pablo (por defecto): " + edades.getOrDefault("Pablo", 0));
System.out.println("¿Está Lucía? " + edades.containsKey("Lucía")
+ "; ¿alguien con 40? " + edades.containsValue(40));
edades.putIfAbsent("Lucía", 99); // no cambia: ya existe
edades.remove("Marta");
System.out.println("Tamaño: " + edades.size());
// Recorridos
Map<String, Integer> ordenado = new TreeMap<>(edades); // ordena por clave
ordenado.put("Beatriz", 19);
System.out.println("Claves: " + ordenado.keySet());
System.out.println("Valores: " + ordenado.values());
for (Map.Entry<String, Integer> e : ordenado.entrySet()) {
System.out.println(" " + e.getKey() + " tiene " + e.getValue() + " años");
}
// LinkedHashMap: recuerda el orden de inserción
Map<String, String> capitales = new LinkedHashMap<>();
capitales.put("Portugal", "Lisboa");
capitales.put("Francia", "París");
capitales.put("Italia", "Roma");
System.out.println(capitales);
}
}
Salida:
Valor anterior de Andrés: 25
Edad de Marta: 40
Edad de Pablo: null
Edad de Pablo (por defecto): 0
¿Está Lucía? true; ¿alguien con 40? true
Tamaño: 2
Claves: [Andrés, Beatriz, Lucía]
Valores: [26, 19, 31]
Andrés tiene 26 años
Beatriz tiene 19 años
Lucía tiene 31 años
{Portugal=Lisboa, Francia=París, Italia=Roma}
| Método | Qué hace |
|---|---|
put(k, v) | Asocia v a k. Si k ya existía, reemplaza y devuelve el valor anterior |
get(k) | Valor asociado a k, o null si no existe |
getOrDefault(k, def) | Valor de k, o def si no existe |
containsKey(k) / containsValue(v) | ¿Existe la clave? ¿Algún valor igual a v? (el segundo es O(n)) |
putIfAbsent(k, v) | Solo añade si la clave no existía |
remove(k) | Borra la entrada y devuelve su valor |
keySet() / values() / entrySet() | Vistas de las claves, los valores y las parejas (Map.Entry) |
| Clase | Orden de las claves | Úsala cuando… |
|---|---|---|
HashMap | Sin orden garantizado | Caso general (el más usado) |
LinkedHashMap | Orden de inserción | Quieres recorrer en el orden en que añadiste |
TreeMap | Ordenadas (natural o Comparator) | Necesitas las claves ordenadas |
💡 Consejo: Para recorrer un mapa usando claves y valores, recorre
entrySet(), nokeySet()+get(k): es más claro y más eficiente.
5.1. Contar frecuencias
Contar cuántas veces aparece cada cosa es uno de los usos más frecuentes de un mapa:
import java.util.Map;
import java.util.TreeMap;
public class Frecuencias {
public static void main(String[] args) {
String texto = "abracadabra";
Map<Character, Integer> cuenta = new TreeMap<>();
for (char c : texto.toCharArray()) {
cuenta.put(c, cuenta.getOrDefault(c, 0) + 1); // patrón "contar"
}
System.out.println(cuenta);
}
}
Salida:
{a=5, b=2, c=1, d=1, r=2}
cuenta.put(c, cuenta.getOrDefault(c, 0) + 1) se lee: “el nuevo valor es el que había (o 0 si no había) más uno”. Existe una forma más corta, cuenta.merge(c, 1, Integer::sum): si la clave no existe pone 1, y si existe suma 1 al valor anterior.
6. Pilas y colas: Deque, Queue y PriorityQueue
- Una pila (stack) es LIFO (last in, first out): como una pila de platos, se coloca y se quita por arriba. Ejemplos: el botón “atrás” del navegador, deshacer en un editor, la pila de llamadas del módulo 15.
- Una cola (queue) es FIFO (first in, first out): como la cola del supermercado, se entra por detrás y se sale por delante.
- Una cola de prioridad sirve siempre primero al elemento de mayor prioridad, no al que llegó antes: como las urgencias de un hospital.
ArrayDeque (cola doble: se puede usar por los dos extremos) sirve para las dos primeras:
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.PriorityQueue;
import java.util.Queue;
public class PilaCola {
public static void main(String[] args) {
// Pila (LIFO): el último en entrar es el primero en salir
Deque<String> pila = new ArrayDeque<>();
pila.push("página 1");
pila.push("página 2");
pila.push("página 3");
System.out.println("Pila: " + pila + ", cima: " + pila.peek());
System.out.println("Atrás -> " + pila.pop() + ", atrás -> " + pila.pop());
System.out.println("Queda: " + pila);
// Cola (FIFO): el primero en entrar es el primero en salir
Queue<String> cola = new ArrayDeque<>();
cola.offer("cliente A");
cola.offer("cliente B");
cola.offer("cliente C");
System.out.println("Cola: " + cola + ", siguiente: " + cola.peek());
System.out.println("Atendido: " + cola.poll() + ", atendido: " + cola.poll());
System.out.println("Queda: " + cola);
// Cola de prioridad: sale siempre el menor (o según un Comparator)
PriorityQueue<Integer> urgencias = new PriorityQueue<>();
urgencias.offer(5);
urgencias.offer(1);
urgencias.offer(3);
urgencias.offer(2);
System.out.print("Por prioridad:");
while (!urgencias.isEmpty()) {
System.out.print(" " + urgencias.poll());
}
System.out.println();
}
}
Salida:
Pila: [página 3, página 2, página 1], cima: página 3
Atrás -> página 3, atrás -> página 2
Queda: [página 1]
Cola: [cliente A, cliente B, cliente C], siguiente: cliente A
Atendido: cliente A, atendido: cliente B
Queda: [cliente C]
Por prioridad: 1 2 3 5
| Uso | Añadir | Quitar | Consultar sin quitar |
|---|---|---|---|
Pila (Deque) | push(e) | pop() | peek() |
Cola (Queue) | offer(e) | poll() | peek() |
💡 Consejo:
poll()ypeek()devuelvennullsi la estructura está vacía;pop(),remove()yelement()lanzanNoSuchElementException. No uses la antigua claseStack: está desaconsejada;ArrayDequees su sustituta.
7. Ordenar: Comparable y Comparator
Para ordenar, Java necesita saber cómo comparar dos elementos. Hay dos mecanismos:
Comparable<T> | Comparator<T> | |
|---|---|---|
| Dónde se define | Dentro de la clase (implements Comparable<Alumno>) | Fuera, como un objeto aparte |
| Método | int compareTo(T otro) | int compare(T a, T b) |
| Cuántos | Uno: el orden natural | Tantos como quieras |
| Ejemplos en Java | String (alfabético), Integer (numérico), LocalDate | Comparator.comparing(...), Comparator.reverseOrder() |
Ambos métodos devuelven un negativo si el primero va antes, 0 si son equivalentes y un positivo si va después.
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
class Alumno implements Comparable<Alumno> {
private final String nombre;
private final String grupo;
private final double nota;
Alumno(String nombre, String grupo, double nota) {
this.nombre = nombre;
this.grupo = grupo;
this.nota = nota;
}
public String getNombre() { return nombre; }
public String getGrupo() { return grupo; }
public double getNota() { return nota; }
// Orden NATURAL: por nombre
@Override
public int compareTo(Alumno otro) {
return this.nombre.compareTo(otro.nombre);
}
@Override
public String toString() {
return nombre + "(" + grupo + ", " + nota + ")";
}
}
public class Ordenar {
public static void main(String[] args) {
List<Alumno> clase = new ArrayList<>(List.of(
new Alumno("Marta", "B", 7.5), new Alumno("Álex", "A", 9.0),
new Alumno("Carlos", "A", 6.0), new Alumno("Beatriz", "B", 9.0),
new Alumno("Diego", "A", 7.5)));
Collections.sort(clase); // orden natural (Comparable)
System.out.println("Por nombre: " + clase);
clase.sort(Comparator.comparing(Alumno::getNota)); // un Comparator
System.out.println("Por nota: " + clase);
clase.sort(Comparator.comparing(Alumno::getNota).reversed());
System.out.println("Nota desc.: " + clase);
clase.sort(Comparator.comparing(Alumno::getGrupo)
.thenComparing(Alumno::getNota, Comparator.reverseOrder())
.thenComparing(Alumno::getNombre));
System.out.println("Grupo, nota desc., nombre: " + clase);
// Un Comparator escrito como lambda: por longitud del nombre
clase.sort((a1, a2) -> Integer.compare(a1.getNombre().length(), a2.getNombre().length()));
System.out.println("Por longitud del nombre: " + clase);
// Orden alfabético "de diccionario" en español (Á junto a A)
clase.sort(Comparator.comparing(Alumno::getNombre, Collator.getInstance(Locale.of("es", "ES"))));
System.out.println("Por nombre (español): " + clase);
}
}
Salida:
Por nombre: [Beatriz(B, 9.0), Carlos(A, 6.0), Diego(A, 7.5), Marta(B, 7.5), Álex(A, 9.0)]
Por nota: [Carlos(A, 6.0), Diego(A, 7.5), Marta(B, 7.5), Beatriz(B, 9.0), Álex(A, 9.0)]
Nota desc.: [Beatriz(B, 9.0), Álex(A, 9.0), Diego(A, 7.5), Marta(B, 7.5), Carlos(A, 6.0)]
Grupo, nota desc., nombre: [Álex(A, 9.0), Diego(A, 7.5), Carlos(A, 6.0), Beatriz(B, 9.0), Marta(B, 7.5)]
Por longitud del nombre: [Álex(A, 9.0), Diego(A, 7.5), Marta(B, 7.5), Carlos(A, 6.0), Beatriz(B, 9.0)]
Por nombre (español): [Álex(A, 9.0), Beatriz(B, 9.0), Carlos(A, 6.0), Diego(A, 7.5), Marta(B, 7.5)]
| Construcción | Significado |
|---|---|
Collections.sort(lista) / lista.sort(null) | Orden natural (Comparable) |
lista.sort(Comparator.comparing(Alumno::getNota)) | Por un atributo |
.reversed() | Invierte el orden |
.thenComparing(Alumno::getNombre) | Criterio de desempate |
.thenComparing(Alumno::getNota, Comparator.reverseOrder()) | Desempate en orden inverso |
(a, b) -> Integer.compare(a.x(), b.x()) | Comparator escrito como lambda |
⚠️ Importante: Fíjate en la primera línea de la salida:
Álexaparece al final.String.compareTo()compara códigos Unicode, y laÁva después de todas las letras sin tilde. Para ordenar texto “como en el diccionario” usa unCollatordel idioma, como en la última línea del ejemplo.
💡 Consejo: Nunca compares con una resta (
return this.edad - otro.edad;): con números grandes puede desbordarse. UsaInteger.compare(a, b)oDouble.compare(a, b).
8. La clase Collections y las colecciones inmutables
8.1. Utilidades de Collections
La clase Collections (con s final; no confundir con la interfaz Collection) tiene métodos estáticos para trabajar con colecciones, igual que Arrays los tiene para arrays:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class UtilidadesCollections {
public static void main(String[] args) {
List<Integer> numeros = new ArrayList<>(List.of(42, 7, 19, 7, 88, 3));
System.out.println("Máximo: " + Collections.max(numeros) + ", mínimo: " + Collections.min(numeros));
System.out.println("Veces que aparece el 7: " + Collections.frequency(numeros, 7));
Collections.sort(numeros);
System.out.println("Ordenada: " + numeros);
System.out.println("¿Dónde está el 19? " + Collections.binarySearch(numeros, 19));
Collections.reverse(numeros);
System.out.println("Invertida: " + numeros);
Collections.shuffle(numeros, new Random(42)); // semilla fija: siempre el mismo orden
System.out.println("Barajada: " + numeros);
List<Integer> soloLectura = Collections.unmodifiableList(numeros);
System.out.println("Vista de solo lectura: " + soloLectura);
numeros.add(100); // la original sí cambia...
System.out.println("...y la vista lo refleja: " + soloLectura);
soloLectura.add(1); // UnsupportedOperationException
}
}
Salida:
Máximo: 88, mínimo: 3
Veces que aparece el 7: 2
Ordenada: [3, 7, 7, 19, 42, 88]
¿Dónde está el 19? 3
Invertida: [88, 42, 19, 7, 7, 3]
Barajada: [42, 88, 7, 3, 7, 19]
Vista de solo lectura: [42, 88, 7, 3, 7, 19]
...y la vista lo refleja: [42, 88, 7, 3, 7, 19, 100]
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1093)
at UtilidadesCollections.main(UtilidadesCollections.java:25)
binarySearch es la búsqueda binaria del módulo 08 (requiere la lista ordenada). unmodifiableList devuelve una vista de solo lectura: no se puede modificar a través de ella, pero refleja los cambios de la lista original.
8.2. List.of, Set.of y Map.of (Java 9)
Para crear colecciones pequeñas con valores fijos existen los métodos de fábrica of, que crean colecciones inmutables:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Inmutables {
public static void main(String[] args) {
List<String> dias = List.of("lunes", "martes", "miércoles");
Map<String, Integer> notas = Map.of("Ana", 8, "Luis", 6);
Set<Integer> primos = Set.of(2, 3, 5, 7);
System.out.println(dias + " " + notas.get("Ana") + " " + primos.contains(5));
List<String> copia = new ArrayList<>(dias); // copia modificable
copia.add("jueves");
System.out.println("Copia modificable: " + copia);
List<String> congelada = List.copyOf(copia); // copia inmutable
System.out.println("Copia inmutable: " + congelada);
try {
dias.add("jueves");
} catch (UnsupportedOperationException e) {
System.out.println("No se puede modificar: " + e);
}
try {
Set.of(1, 2, 2);
} catch (IllegalArgumentException e) {
System.out.println("Set.of con duplicados: " + e.getMessage());
}
}
}
Salida:
[lunes, martes, miércoles] 8 true
Copia modificable: [lunes, martes, miércoles, jueves]
Copia inmutable: [lunes, martes, miércoles, jueves]
No se puede modificar: java.lang.UnsupportedOperationException
Set.of con duplicados: duplicate element: 2
List.copyOf(c) crea una copia inmutable (no refleja cambios posteriores del original), a diferencia de Collections.unmodifiableList, que es una vista. Ninguna de las colecciones creadas con of o copyOf admite null.
⚠️ Importante: El orden de recorrido de
Set.ofyMap.ofno está definido y puede cambiar entre ejecuciones. Si necesitas orden, usanew TreeSet<>(Set.of(...))o unLinkedHashMap. Y si necesitas una lista que puedas modificar, crea una copia:new ArrayList<>(List.of(...)).
8.3. Colecciones secuenciadas (Java 21)
Java 21 añadió las interfaces SequencedCollection, SequencedSet y SequencedMap para las colecciones que tienen un orden de encuentro definido (listas, Deque, LinkedHashSet, TreeSet, LinkedHashMap, TreeMap…). Ofrecen métodos uniformes para trabajar con el primer y el último elemento:
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SequencedMap;
import java.util.SequencedSet;
public class Secuenciadas {
public static void main(String[] args) {
List<String> lista = new ArrayList<>(List.of("b", "c", "d"));
lista.addFirst("a"); // Java 21
lista.addLast("e");
System.out.println(lista + " primero=" + lista.getFirst() + " último=" + lista.getLast());
System.out.println("Al revés: " + lista.reversed()); // vista invertida, no copia
SequencedSet<Integer> visitas = new LinkedHashSet<>(List.of(30, 10, 20));
System.out.println("Primera visita: " + visitas.getFirst() + ", al revés: " + visitas.reversed());
SequencedMap<String, Integer> ranking = new LinkedHashMap<>();
ranking.put("Eva", 120);
ranking.put("Iván", 95);
ranking.putFirst("Sara", 150); // inserta al principio
System.out.println(ranking + " primero=" + ranking.firstEntry() + " último=" + ranking.lastEntry());
}
}
Salida:
[a, b, c, d, e] primero=a último=e
Al revés: [e, d, c, b, a]
Primera visita: 30, al revés: [20, 10, 30]
{Sara=150, Eva=120, Iván=95} primero=Sara=150 último=Iván=95
Antes de Java 21 había que escribir lista.get(0) o lista.get(lista.size() - 1), y cada tipo de colección tenía sus propios nombres para lo mismo.
⚠️ Importante: Un
HashSeto unHashMapno son secuenciados (no tienen orden definido), así que no tienen estos métodos.
9. Clases y métodos genéricos propios
Las colecciones son genéricas, pero tú también puedes escribir tus propias clases y métodos genéricos. El parámetro de tipo (por convención, una letra mayúscula: T de type, E de element, K de key, V de value) actúa como un “hueco” que se rellena al usar la clase.
import java.util.List;
// Clase genérica con un parámetro de tipo
class Caja<T> {
private T contenido;
public void guardar(T contenido) {
this.contenido = contenido;
}
public T sacar() {
return contenido;
}
public boolean estaVacia() {
return contenido == null;
}
}
// Clase genérica con dos parámetros de tipo
class Par<K, V> {
private final K clave;
private final V valor;
public Par(K clave, V valor) {
this.clave = clave;
this.valor = valor;
}
public K getClave() { return clave; }
public V getValor() { return valor; }
@Override
public String toString() {
return "(" + clave + ", " + valor + ")";
}
}
public class Genericos {
// Método genérico con tipo ACOTADO: T debe poder compararse
static <T extends Comparable<T>> T maximo(List<T> lista) {
T max = lista.get(0);
for (T elemento : lista) {
if (elemento.compareTo(max) > 0) {
max = elemento;
}
}
return max;
}
public static void main(String[] args) {
Caja<String> cajaTexto = new Caja<>();
cajaTexto.guardar("un libro");
String s = cajaTexto.sacar(); // sin cast
System.out.println("Caja de texto: " + s.toUpperCase());
Caja<Integer> cajaNumero = new Caja<>();
System.out.println("¿Vacía? " + cajaNumero.estaVacia());
cajaNumero.guardar(42); // autoboxing: int -> Integer
int n = cajaNumero.sacar() + 1;
System.out.println("Caja de número + 1 = " + n);
// cajaNumero.guardar("hola"); // ERROR de compilación
Par<String, Double> nota = new Par<>("Matemáticas", 8.5);
System.out.println(nota + " -> clave " + nota.getClave() + ", valor " + nota.getValor());
System.out.println("Máximo: " + maximo(List.of(3, 17, 8)) + " y " + maximo(List.of("pera", "zanahoria", "kiwi")));
}
}
Salida:
Caja de texto: UN LIBRO
¿Vacía? true
Caja de número + 1 = 43
(Matemáticas, 8.5) -> clave Matemáticas, valor 8.5
Máximo: 17 y zanahoria
Si intentas guardar un texto en una Caja<Integer>, el error aparece al compilar, con un mensaje muy claro:
GenericoError.java:9: error: method guardar in class Caja<T> cannot be applied to given types;
caja.guardar("hola");
^
required: Integer
found: String
reason: argument mismatch; String cannot be converted to Integer
where T is a type-variable:
T extends Object declared in class Caja
1 error
error: compilation failed
| Sintaxis | Significado |
|---|---|
class Caja<T> { ... } | Clase genérica con un parámetro de tipo |
class Par<K, V> { ... } | Dos parámetros de tipo |
static <T> T primero(List<T> l) | Método genérico: el <T> va antes del tipo de retorno |
<T extends Comparable<T>> | Tipo acotado: T debe implementar Comparable<T> (así podemos llamar a compareTo) |
List<? extends Number> | Comodín: “una lista de algún subtipo de Number” (lo verás en el Curso Avanzado) |
📘 Concepto: Los genéricos existen solo en tiempo de compilación: el compilador comprueba los tipos y luego los “borra” (type erasure). Por eso no puedes hacer
new T()ninew T[10], y por esoList<int>no es válido.
10. Elegir la colección adecuada
Figura 4. Cómo elegir la colección adecuada.
11. Ejemplos completos
11.1. Agenda de contactos
Un TreeMap cuya clave es el nombre en minúsculas permite buscar sin distinguir mayúsculas, impedir contactos repetidos y listar en orden alfabético:
import java.util.Map;
import java.util.TreeMap;
record Contacto(String nombre, String telefono, String email) { }
class AgendaContactos {
// Clave: nombre en minúsculas (para buscar sin distinguir mayúsculas)
private final Map<String, Contacto> contactos = new TreeMap<>();
public boolean anadir(Contacto c) {
String clave = c.nombre().toLowerCase();
if (contactos.containsKey(clave)) {
return false;
}
contactos.put(clave, c);
return true;
}
public Contacto buscar(String nombre) {
return contactos.get(nombre.toLowerCase());
}
public boolean borrar(String nombre) {
return contactos.remove(nombre.toLowerCase()) != null;
}
public void listar() {
for (Contacto c : contactos.values()) {
System.out.printf(" %-16s %-11s %s%n", c.nombre(), c.telefono(), c.email());
}
}
}
public class Agenda {
public static void main(String[] args) {
AgendaContactos agenda = new AgendaContactos();
agenda.anadir(new Contacto("Pedro Gil", "600111222", "pedro@ejemplo.es"));
agenda.anadir(new Contacto("Ana Ruiz", "611333444", "ana@ejemplo.es"));
agenda.anadir(new Contacto("Andrés Sanz", "622555666", "andres@ejemplo.es"));
System.out.println("¿Añadido ana ruiz repetido? "
+ agenda.anadir(new Contacto("ANA RUIZ", "699999999", "otra@ejemplo.es")));
System.out.println("Agenda (ordenada por nombre):");
agenda.listar();
System.out.println("Buscar 'pedro gil': " + agenda.buscar("pedro gil"));
System.out.println("¿Borrado Pedro Gil? " + agenda.borrar("Pedro Gil"));
System.out.println("Buscar 'pedro gil': " + agenda.buscar("pedro gil"));
}
}
Salida:
¿Añadido ana ruiz repetido? false
Agenda (ordenada por nombre):
Ana Ruiz 611333444 ana@ejemplo.es
Andrés Sanz 622555666 andres@ejemplo.es
Pedro Gil 600111222 pedro@ejemplo.es
Buscar 'pedro gil': Contacto[nombre=Pedro Gil, telefono=600111222, email=pedro@ejemplo.es]
¿Borrado Pedro Gil? true
Buscar 'pedro gil': null
11.2. Inventario de un almacén
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Inventario {
private final Map<String, Integer> stock = new TreeMap<>();
public void entrada(String producto, int unidades) {
stock.put(producto, stock.getOrDefault(producto, 0) + unidades);
}
public boolean salida(String producto, int unidades) {
int actual = stock.getOrDefault(producto, 0);
if (unidades > actual) {
return false;
}
if (actual == unidades) {
stock.remove(producto); // sin existencias: lo quitamos del mapa
} else {
stock.put(producto, actual - unidades);
}
return true;
}
public List<String> bajoMinimos(int minimo) {
List<String> resultado = new ArrayList<>();
for (Map.Entry<String, Integer> e : stock.entrySet()) {
if (e.getValue() < minimo) {
resultado.add(e.getKey() + " (" + e.getValue() + ")");
}
}
return resultado;
}
public static void main(String[] args) {
Inventario inv = new Inventario();
inv.entrada("tornillos", 500);
inv.entrada("tuercas", 120);
inv.entrada("arandelas", 8);
inv.entrada("tuercas", 30);
System.out.println("Stock: " + inv.stock);
System.out.println("Salida de 600 tornillos: " + inv.salida("tornillos", 600));
System.out.println("Salida de 495 tornillos: " + inv.salida("tornillos", 495));
System.out.println("Salida de 8 arandelas: " + inv.salida("arandelas", 8));
System.out.println("Stock: " + inv.stock);
System.out.println("Bajo mínimos (< 10): " + inv.bajoMinimos(10));
}
}
Salida:
Stock: {arandelas=8, tornillos=500, tuercas=150}
Salida de 600 tornillos: false
Salida de 495 tornillos: true
Salida de 8 arandelas: true
Stock: {tornillos=5, tuercas=150}
Bajo mínimos (< 10): [tornillos (5)]
11.3. Contar palabras de un texto
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ContarPalabras {
public static void main(String[] args) {
String texto = """
En un lugar de la Mancha, de cuyo nombre no quiero acordarme,
no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero,
adarga antigua, rocín flaco y galgo corredor.
""";
Map<String, Integer> frecuencias = new HashMap<>();
for (String palabra : texto.toLowerCase().split("[^\\p{L}]+")) { // separa por lo que no sea letra
if (!palabra.isEmpty()) {
frecuencias.put(palabra, frecuencias.getOrDefault(palabra, 0) + 1);
}
}
System.out.println("Palabras distintas: " + frecuencias.size());
// Ordenar las entradas: más frecuentes primero; a igualdad, alfabéticamente
List<Map.Entry<String, Integer>> entradas = new ArrayList<>(frecuencias.entrySet());
entradas.sort(Map.Entry.<String, Integer>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()));
System.out.println("Las 5 más frecuentes:");
for (int i = 0; i < 5; i++) {
Map.Entry<String, Integer> e = entradas.get(i);
System.out.printf(" %-10s %d%n", e.getKey(), e.getValue());
}
}
}
Salida:
Palabras distintas: 27
Las 5 más frecuentes:
de 4
en 2
no 2
un 2
acordarme 1
La expresión regular [^\\p{L}]+ (módulo 09) significa “uno o más caracteres que no sean letras”, así que separa por espacios y signos de puntuación y respeta las tildes y la ñ. Para ordenar por frecuencia, se copian las entradas del mapa en una lista y se ordena con un Comparator doble: primero por valor descendente y, a igualdad, por clave.
11.4. Cola de atención
import java.util.ArrayDeque;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
record Paciente(String nombre, int gravedad, int llegada) { } // gravedad: 1 (leve) a 5 (crítica)
public class ColaAtencion {
public static void main(String[] args) {
Paciente[] llegadas = {
new Paciente("Rosa", 2, 1), new Paciente("Tomás", 5, 2), new Paciente("Inés", 3, 3),
new Paciente("Hugo", 5, 4), new Paciente("Celia", 1, 5)
};
// Ventanilla normal: por orden de llegada (FIFO)
Queue<Paciente> ventanilla = new ArrayDeque<>();
// Urgencias: primero la mayor gravedad; a igual gravedad, quien llegó antes
Queue<Paciente> urgencias = new PriorityQueue<>(
Comparator.comparingInt(Paciente::gravedad).reversed()
.thenComparingInt(Paciente::llegada));
for (Paciente p : llegadas) {
ventanilla.offer(p);
urgencias.offer(p);
}
System.out.print("Ventanilla:");
while (!ventanilla.isEmpty()) {
System.out.print(" " + ventanilla.poll().nombre());
}
System.out.println();
System.out.print("Urgencias: ");
while (!urgencias.isEmpty()) {
Paciente p = urgencias.poll();
System.out.print(" " + p.nombre() + "(" + p.gravedad() + ")");
}
System.out.println();
}
}
Salida:
Ventanilla: Rosa Tomás Inés Hugo Celia
Urgencias: Tomás(5) Hugo(5) Inés(3) Rosa(2) Celia(1)
La misma secuencia de llegadas se atiende de forma distinta según la estructura: ArrayDeque respeta el orden de llegada; PriorityQueue con un Comparator atiende primero la mayor gravedad y, a igualdad, a quien llegó antes (Tomás antes que Hugo).
12. Errores comunes
| Error | Síntoma | Solución |
|---|---|---|
Tipos crudos (new ArrayList() sin <>) | Avisos y ClassCastException | Usa siempre genéricos |
| Borrar dentro de un for-each | ConcurrentModificationException | Iterator.remove() o removeIf |
lista.remove(1) en una List<Integer> queriendo borrar el valor | Se borra la posición 1 | lista.remove(Integer.valueOf(1)) |
Objetos propios en HashSet/HashMap sin equals/hashCode | Duplicados, contains devuelve false | Sobrescríbelos o usa un record |
map.get(k) de una clave inexistente usado como int | NullPointerException al desempaquetar | getOrDefault(k, 0) o containsKey |
Modificar una colección de List.of | UnsupportedOperationException | Copia: new ArrayList<>(List.of(...)) |
Depender del orden de HashSet/HashMap | Orden “aleatorio” | LinkedHashSet/LinkedHashMap o TreeSet/TreeMap |
13. Ejercicios
Ejercicio 1: Quitar duplicados conservando el orden
Escribe dos métodos genéricos: sinDuplicados(List<T>), que devuelva una lista sin repetidos conservando el orden de primera aparición, y repetidos(List<T>), que devuelva el conjunto de elementos que aparecen más de una vez. Pruébalos con una lista de correos y otra de números.
Ver solución
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class E1Duplicados {
static <T> List<T> sinDuplicados(List<T> lista) {
return new ArrayList<>(new LinkedHashSet<>(lista)); // conserva el primer orden de aparición
}
static <T> Set<T> repetidos(List<T> lista) {
Set<T> vistos = new LinkedHashSet<>();
Set<T> repes = new LinkedHashSet<>();
for (T e : lista) {
if (!vistos.add(e)) { // add devuelve false si ya estaba
repes.add(e);
}
}
return repes;
}
public static void main(String[] args) {
List<String> correos = List.of("ana@x.es", "luis@x.es", "ana@x.es", "eva@x.es", "luis@x.es", "ana@x.es");
System.out.println("Sin duplicados: " + sinDuplicados(correos));
System.out.println("Repetidos: " + repetidos(correos));
System.out.println("Números: " + sinDuplicados(List.of(5, 1, 5, 2, 1, 9)));
}
}
Salida:
Sin duplicados: [ana@x.es, luis@x.es, eva@x.es]
Repetidos: [ana@x.es, luis@x.es]
Números: [5, 1, 2, 9]
new LinkedHashSet<>(lista) elimina duplicados conservando el orden, y new ArrayList<>(...) lo vuelve a convertir en lista. En repetidos se aprovecha que add devuelve false cuando el elemento ya estaba. Los métodos son genéricos (<T>), así que funcionan con String, Integer o cualquier tipo con equals y hashCode correctos.
Ejercicio 2: Notas por alumno
A partir de un array de registros con el formato "Luis:7.5", construye un Map<String, List<Double>> con todas las notas de cada alumno (ordenado por nombre), muestra sus notas y su media, e indica quién tiene la mejor media.
Ver solución
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class E2NotasAlumnos {
public static void main(String[] args) {
String[] registros = {"Luis:7.5", "Ana:9", "Luis:5", "Marta:6.5", "Ana:8", "Luis:6"};
Map<String, List<Double>> notas = new TreeMap<>();
for (String r : registros) {
String[] partes = r.split(":");
String alumno = partes[0];
double nota = Double.parseDouble(partes[1]);
List<Double> lista = notas.get(alumno);
if (lista == null) { // primera nota de este alumno
lista = new ArrayList<>();
notas.put(alumno, lista);
}
lista.add(nota);
}
String mejor = null;
double mejorMedia = -1;
for (Map.Entry<String, List<Double>> e : notas.entrySet()) {
double suma = 0;
for (double n : e.getValue()) {
suma += n;
}
double media = suma / e.getValue().size();
System.out.printf("%-6s %-18s media %.2f%n", e.getKey(), e.getValue(), media);
if (media > mejorMedia) {
mejorMedia = media;
mejor = e.getKey();
}
}
System.out.println("Mejor media: " + mejor);
}
}
Salida:
Ana [9.0, 8.0] media 8.50
Luis [7.5, 5.0, 6.0] media 6.17
Marta [6.5] media 6.50
Mejor media: Ana
Es el patrón agrupar: para cada registro se busca la lista del alumno y, si no existe todavía (get devuelve null), se crea y se guarda en el mapa. Como la lista es un objeto, al hacer lista.add(nota) se modifica la que está dentro del mapa (es la misma referencia, módulo 10). En el Curso Avanzado verás que notas.computeIfAbsent(alumno, k -> new ArrayList<>()).add(nota) hace lo mismo en una línea.
Ejercicio 3: Paréntesis equilibrados
Escribe boolean equilibrado(String expr) que compruebe, usando una pila, si los paréntesis (), corchetes [] y llaves {} de una expresión están bien emparejados y anidados.
Ver solución
import java.util.ArrayDeque;
import java.util.Deque;
public class E3Parentesis {
static boolean equilibrado(String expr) {
Deque<Character> pila = new ArrayDeque<>();
for (char c : expr.toCharArray()) {
switch (c) {
case '(', '[', '{' -> pila.push(c);
case ')', ']', '}' -> {
if (pila.isEmpty()) {
return false; // cierre sin apertura
}
char abierto = pila.pop();
if ((c == ')' && abierto != '(') || (c == ']' && abierto != '[')
|| (c == '}' && abierto != '{')) {
return false; // no casan
}
}
default -> { } // cualquier otro carácter se ignora
}
}
return pila.isEmpty(); // no debe quedar nada abierto
}
public static void main(String[] args) {
String[] pruebas = {"(a + b) * [c - {d / e}]", "((1 + 2)", "{[()]}", "([)]", "x)", ""};
for (String p : pruebas) {
System.out.printf("%-26s %s%n", "\"" + p + "\"", equilibrado(p) ? "equilibrado" : "NO equilibrado");
}
}
}
Salida:
"(a + b) * [c - {d / e}]" equilibrado
"((1 + 2)" NO equilibrado
"{[()]}" equilibrado
"([)]" NO equilibrado
"x)" NO equilibrado
"" equilibrado
Cada apertura se apila; cada cierre debe coincidir con la última apertura pendiente, que es justo la cima de la pila. Hay tres formas de fallar: un cierre sin apertura (pila vacía), un cierre que no casa con la apertura (([)]) y aperturas que quedan sin cerrar al final (((1 + 2)). Los compiladores usan esta misma técnica.
Ejercicio 4: Catálogo ordenado
Crea un record Producto(String nombre, String categoria, double precio) y ordena un catálogo por categoría, después por precio de mayor a menor y, a igual precio, por nombre. Muéstralo agrupado por categoría.
Ver solución
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
record Producto(String nombre, String categoria, double precio) { }
public class E4Productos {
public static void main(String[] args) {
List<Producto> catalogo = new ArrayList<>(List.of(
new Producto("Ratón", "Informática", 15.99),
new Producto("Sartén", "Hogar", 24.50),
new Producto("Teclado", "Informática", 45.00),
new Producto("Lámpara", "Hogar", 24.50),
new Producto("Monitor", "Informática", 189.00),
new Producto("Cojín", "Hogar", 9.95)));
catalogo.sort(Comparator.comparing(Producto::categoria)
.thenComparing(Producto::precio, Comparator.reverseOrder())
.thenComparing(Producto::nombre));
String categoriaActual = "";
for (Producto p : catalogo) {
if (!p.categoria().equals(categoriaActual)) {
categoriaActual = p.categoria();
System.out.println(categoriaActual + ":");
}
System.out.printf(" %-8s %7.2f €%n", p.nombre(), p.precio());
}
}
}
Salida:
Hogar:
Lámpara 24.50 €
Sartén 24.50 €
Cojín 9.95 €
Informática:
Monitor 189.00 €
Teclado 45.00 €
Ratón 15.99 €
Con records, las referencias a métodos son los propios accesores (Producto::precio). Como la lista está ordenada por categoría, para agruparla basta con imprimir la cabecera cada vez que la categoría cambia. Para encontrar el más barato bastaría con Collections.min(catalogo, Comparator.comparing(Producto::precio)).
Ejercicio 5: Pila genérica
Implementa una clase genérica Pila<T> con apilar, desapilar, cima y estaVacia, usando internamente un ArrayList. desapilar y cima deben lanzar NoSuchElementException si la pila está vacía. Úsala para simular el “deshacer” de un editor.
Ver solución
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
class Pila<T> {
private final List<T> elementos = new ArrayList<>();
public void apilar(T elemento) {
elementos.add(elemento); // la cima es el final de la lista
}
public T desapilar() {
if (estaVacia()) {
throw new NoSuchElementException("La pila está vacía");
}
return elementos.removeLast(); // Java 21
}
public T cima() {
if (estaVacia()) {
throw new NoSuchElementException("La pila está vacía");
}
return elementos.getLast();
}
public boolean estaVacia() {
return elementos.isEmpty();
}
@Override
public String toString() {
return "Pila" + elementos + " <- cima";
}
}
public class E5PilaGenerica {
public static void main(String[] args) {
Pila<String> deshacer = new Pila<>();
deshacer.apilar("escribir 'Hola'");
deshacer.apilar("poner negrita");
deshacer.apilar("borrar línea");
System.out.println(deshacer);
System.out.println("Deshacer: " + deshacer.desapilar());
System.out.println("Siguiente a deshacer: " + deshacer.cima());
Pila<Integer> numeros = new Pila<>();
numeros.apilar(1);
System.out.println("Saco " + numeros.desapilar());
try {
numeros.desapilar();
} catch (NoSuchElementException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Salida:
Pila[escribir 'Hola', poner negrita, borrar línea] <- cima
Deshacer: borrar línea
Siguiente a deshacer: poner negrita
Saco 1
Error: La pila está vacía
Es una clase envoltorio que reutiliza ArrayList por composición y solo expone las operaciones de una pila: nadie puede acceder al elemento del medio. La cima es el final de la lista, porque removeLast() en un ArrayList es O(1), mientras que quitar el primero obligaría a desplazar todo.
Ejercicio 6: Grupos de anagramas
Dos palabras son anagramas si tienen las mismas letras en distinto orden (amor, roma, mora). Agrupa una lista de palabras en un Map<String, List<String>> cuya clave sea la “firma” de la palabra (sus letras ordenadas) y muestra solo los grupos con más de una palabra.
Ver solución
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class E6Anagramas {
static String firma(String palabra) {
char[] letras = palabra.toLowerCase().toCharArray();
Arrays.sort(letras);
return new String(letras);
}
public static void main(String[] args) {
String[] palabras = {"amor", "roma", "mora", "ramo", "perro", "saco", "cosa", "caso", "mar", "ram", "luz"};
Map<String, List<String>> grupos = new TreeMap<>();
for (String p : palabras) {
String clave = firma(p);
if (!grupos.containsKey(clave)) {
grupos.put(clave, new ArrayList<>());
}
grupos.get(clave).add(p);
}
for (Map.Entry<String, List<String>> e : grupos.entrySet()) {
if (e.getValue().size() > 1) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
}
}
}
Salida:
acos -> [saco, cosa, caso]
amor -> [amor, roma, mora, ramo]
amr -> [mar, ram]
La clave del ejercicio es elegir bien la clave del mapa: todas las palabras de un mismo grupo producen la misma firma (amor, roma, mora y ramo → amor). Con TreeMap, los grupos salen ordenados por firma. Es la misma idea que el ejercicio 2 (agrupar), con una clave calculada.
Ejercicios propuestos
✏️ Ejercicio 7: Lista de la compra. Escribe un programa de consola con un menú para añadir productos, borrar uno por nombre, mostrar la lista numerada y vaciarla, usando un
ArrayList<String>. Impide añadir productos repetidos sin distinguir mayúsculas.
✏️ Ejercicio 8: Lotería primitiva. Genera combinaciones de 6 números distintos entre 1 y 49 usando un
TreeSet<Integer>yRandom. Después, dada una combinación ganadora, calcula cuántos aciertos tiene cada combinación conretainAll.
✏️ Ejercicio 9: Traductor. Crea un
Map<String, String>español → inglés con al menos 15 palabras y traduce frases palabra a palabra; las desconocidas se muestran entre corchetes. Añade una opción para invertir el diccionario (inglés → español) y detecta si hay traducciones repetidas que impidan invertirlo.
✏️ Ejercicio 10: Ranking de jugadores. Crea un
record Jugador(String nombre, int puntos, int partidas)y muestra el ranking ordenado por puntos (descendente), después por menos partidas jugadas y, por último, por nombre en orden alfabético español (Collator). Muestra también el podio consubList(0, 3).
✏️ Ejercicio 11: Historial del navegador. Simula los botones “atrás” y “adelante” de un navegador con dos
ArrayDequeusados como pilas. Visitar una página nueva vacía la pila de “adelante”. Lee órdenes del teclado (ir <url>,atras,adelante,fin) y muestra la página actual tras cada una.
✏️ Ejercicio 12: Clase genérica
Estadistica<T extends Number>. Guarda una lista de números de cualquier tipo numérico y ofrecemedia(),maximo()yminimo()(devueltos comodouble, usandodoubleValue()). Pruébala conInteger,DoubleyLong.
Resumen
| Concepto | Sintaxis / ejemplo | Idea clave |
|---|---|---|
| Genéricos | List<String> l = new ArrayList<>(); | Tipos comprobados al compilar, sin casts |
| Lista | ArrayList (por defecto), LinkedList | Ordenada por posición, admite duplicados |
| Recorrer y borrar | for-each, Iterator; it.remove(), removeIf | Nunca remove dentro de un for-each |
| Conjunto | HashSet, LinkedHashSet, TreeSet | Sin duplicados; necesita equals/hashCode (o compareTo) |
| Mapa | HashMap, LinkedHashMap, TreeMap | Clave → valor; recorrer con entrySet() |
| Pila / cola | ArrayDeque: push/pop, offer/poll | LIFO / FIFO |
| Prioridad | PriorityQueue (+ Comparator) | Sale primero el más prioritario |
| Orden natural | implements Comparable<T> + compareTo | Un único orden, dentro de la clase |
| Comparator | Comparator.comparing(X::getY).thenComparing(...).reversed() | Tantos órdenes como quieras |
| Inmutables | List.of, Set.of, Map.of, List.copyOf | No se modifican; no admiten null |
| Secuenciadas (Java 21) | getFirst(), getLast(), reversed(), addFirst() | Primer y último elemento de forma uniforme |
| Clase genérica | class Caja<T> { T contenido; } | Reutilizable con cualquier tipo |
| Método genérico | static <T extends Comparable<T>> T maximo(List<T> l) | <T> antes del tipo de retorno; tipos acotados |