HashMap y HashSet en Java
En esta lección
Una lista es perfecta para guardar cosas en orden, pero no para buscar. Si quieres el precio del “café” en una lista de 5000 productos, tienes que recorrerla entera. Y si quieres saber cuántos correos distintos hay en una lista con repetidos, te toca comparar cada uno con todos los demás. Para esos dos problemas existen los mapas (Map) y los conjuntos (Set). En esta lección aprenderás a usar sus implementaciones más comunes: HashMap, LinkedHashMap, TreeMap, HashSet, LinkedHashSet y TreeSet.
Qué es un mapa
Un mapa guarda pares clave-valor, como un diccionario: buscas por la palabra (clave) y obtienes su definición (valor). Ejemplos reales:
- Carta de una cafetería: producto → precio.
- Agenda: nombre → teléfono.
- Inventario: referencia → unidades en stock.
Reglas básicas:
- Las claves no se repiten. Si guardas otra vez la misma clave, se sustituye su valor.
- Los valores sí se pueden repetir.
- Buscar por clave es muy rápido, da igual cuántos elementos haya.
Se declara con dos tipos genéricos, el de la clave y el del valor:
import java.util.HashMap;
import java.util.Map;
Map<String, Double> precios = new HashMap<>();
Operaciones de Map
put y get
precios.put("café", 1.20);
precios.put("tostada", 2.50);
precios.put("zumo", 3.00);
System.out.println(precios.get("café")); // 1.2
System.out.println(precios.get("té")); // null (no existe)
System.out.println(precios.size()); // 3
precios.put("café", 1.40); // la clave ya existe: se sustituye
System.out.println(precios.get("café")); // 1.4
get devuelve null si la clave no existe. Ojo: si guardas ese resultado en un double primitivo, obtendrás un NullPointerException.
getOrDefault, containsKey y remove
System.out.println(precios.getOrDefault("té", 0.0)); // 0.0
System.out.println(precios.containsKey("zumo")); // true
System.out.println(precios.containsValue(2.50)); // true
precios.remove("zumo");
System.out.println(precios.containsKey("zumo")); // false
getOrDefault evita el null: si la clave no está, devuelve el valor que le pases.
putIfAbsent
Añade el par solo si la clave no existe; si existe, no toca nada:
precios.putIfAbsent("café", 9.99); // ya existe: se queda en 1.4
precios.putIfAbsent("té", 1.10); // no existía: se añade
System.out.println(precios.get("café") + " " + precios.get("té")); // 1.4 1.1
Contar con un mapa: merge
Un uso clásico de los mapas es contar apariciones. Por ejemplo, cuántas veces sale cada palabra en un texto. Con getOrDefault:
String texto = "el gato y el perro y el ratón";
Map<String, Integer> cuenta = new HashMap<>();
for (String palabra : texto.split(" ")) {
cuenta.put(palabra, cuenta.getOrDefault(palabra, 0) + 1);
}
System.out.println(cuenta); // {perro=1, ratón=1, gato=1, el=3, y=2}
Y con merge, que hace lo mismo en una línea: “si la clave no existe, pon 1; si existe, suma su valor actual y 1”:
cuenta.merge(palabra, 1, Integer::sum);
Integer::sum es una referencia al método que suma dos enteros; las verás en lambdas y streams.
Recorrer un mapa
Un mapa no tiene posiciones, así que no se recorre con un for de índices. Tiene tres “vistas” que sí se pueden recorrer:
| Método | Devuelve |
|---|---|
keySet() | Un Set con las claves |
values() | Una colección con los valores |
entrySet() | Un Set de pares Map.Entry<K, V> |
Map<String, Integer> stock = new LinkedHashMap<>();
stock.put("camiseta", 12);
stock.put("pantalón", 5);
stock.put("gorra", 0);
// Claves y valores a la vez: la forma más eficiente
for (Map.Entry<String, Integer> e : stock.entrySet()) {
System.out.println(e.getKey() + ": " + e.getValue());
}
// camiseta: 12
// pantalón: 5
// gorra: 0
// Solo los valores
int total = 0;
for (int unidades : stock.values()) {
total += unidades;
}
System.out.println("Total: " + total); // Total: 17
// Con forEach y una lambda de dos parámetros
stock.forEach((producto, unidades) -> System.out.println(producto + " -> " + unidades));
Consejo: si necesitas clave y valor, recorre
entrySet(). RecorrerkeySet()y hacerget(clave)dentro funciona, pero busca cada clave dos veces.
HashMap, LinkedHashMap y TreeMap
Las tres implementan Map y se usan igual. La diferencia está en el orden en que devuelven las claves:
String[] frutas = {"pera", "manzana", "kiwi", "uva", "naranja"};
int[] cajas = {3, 5, 2, 8, 4};
// Metemos los mismos pares en los tres mapas...
System.out.println(hashMap); // {pera=3, manzana=5, uva=8, kiwi=2, naranja=4}
System.out.println(linkedHashMap); // {pera=3, manzana=5, kiwi=2, uva=8, naranja=4}
System.out.println(treeMap); // {kiwi=2, manzana=5, naranja=4, pera=3, uva=8}
HashMap | LinkedHashMap | TreeMap | |
|---|---|---|---|
| Orden | Ninguno garantizado | El de inserción | Ordenado por clave |
| Velocidad | La más rápida | Casi igual | Algo más lenta |
Clave null | Admite una | Admite una | No |
| Úsalo cuando… | El orden no importa (lo normal) | Quieres conservar el orden en que añades | Necesitas las claves ordenadas |
El orden de un HashMap depende de cálculos internos. No te fíes de él: puede cambiar al añadir elementos o entre versiones de Java. TreeMap además tiene métodos como firstKey() y lastKey().
Qué es un conjunto
Un conjunto (Set) es una colección sin elementos repetidos y sin posiciones. Sirve para responder rápido a “¿está este elemento?” y para eliminar duplicados.
Set<String> etiquetas = new HashSet<>();
System.out.println(etiquetas.add("java")); // true (se ha añadido)
etiquetas.add("sql");
System.out.println(etiquetas.add("java")); // false (ya estaba)
System.out.println(etiquetas.size() + " " + etiquetas.contains("sql")); // 2 true
add devuelve false cuando el elemento ya existía, lo que resulta útil para detectar repetidos. Set no tiene get(i): se recorre con for-each o forEach.
Igual que con los mapas, hay tres implementaciones con distinto orden:
HashSet | LinkedHashSet | TreeSet | |
|---|---|---|---|
| Orden | Ninguno garantizado | El de inserción | Ordenado |
| Velocidad | La más rápida | Casi igual | Algo más lenta |
Quitar duplicados
Crear un conjunto a partir de una lista elimina los repetidos de golpe:
List<String> correos = List.of("ana@x.es", "luis@x.es", "ana@x.es", "eva@x.es", "luis@x.es");
Set<String> unicos = new HashSet<>(correos);
System.out.println(unicos); // [luis@x.es, ana@x.es, eva@x.es]
Set<String> enOrden = new LinkedHashSet<>(correos);
System.out.println(enOrden); // [ana@x.es, luis@x.es, eva@x.es]
Set<String> ordenados = new TreeSet<>(correos);
System.out.println(ordenados); // [ana@x.es, eva@x.es, luis@x.es]
Si después necesitas una lista, conviértelo otra vez: new ArrayList<>(enOrden).
Unión, intersección y diferencia
Los métodos addAll, retainAll y removeAll hacen las operaciones clásicas de conjuntos. Como modifican el conjunto sobre el que se llaman, trabaja con una copia. Ejemplo con los alumnos que asisten al gimnasio cada día:
Set<String> lunes = new TreeSet<>(Set.of("Ana", "Luis", "Eva", "Marco"));
Set<String> martes = new TreeSet<>(Set.of("Eva", "Marco", "Sara"));
Set<String> union = new TreeSet<>(lunes);
union.addAll(martes); // vinieron algún día
System.out.println(union); // [Ana, Eva, Luis, Marco, Sara]
Set<String> ambos = new TreeSet<>(lunes);
ambos.retainAll(martes); // vinieron los dos días
System.out.println(ambos); // [Eva, Marco]
Set<String> soloLunes = new TreeSet<>(lunes);
soloLunes.removeAll(martes); // vinieron el lunes pero no el martes
System.out.println(soloLunes); // [Ana, Luis]
Set.of(...), como List.of, crea un conjunto inmutable.
Claves propias: equals y hashCode
HashMap y HashSet necesitan saber cuándo dos objetos son “el mismo”. Para ello usan dos métodos heredados de Object:
hashCode()calcula un número a partir del objeto. Sirve para decidir en qué “cajón” interno se guarda, y es lo que hace la búsqueda tan rápida.equals()confirma si dos objetos del mismo cajón son iguales de verdad.
Con String, Integer o los enums ya vienen bien hechos. Con tus clases, si no los sobrescribes, dos objetos con los mismos datos se consideran distintos:
class ProductoMal {
private final String codigo;
ProductoMal(String codigo) { this.codigo = codigo; }
@Override
public boolean equals(Object o) {
return o instanceof ProductoMal p && codigo.equals(p.codigo);
}
// ¡Falta hashCode!
}
Set<ProductoMal> carrito = new HashSet<>();
carrito.add(new ProductoMal("A1"));
carrito.add(new ProductoMal("A1"));
System.out.println(carrito.size()); // 2 (esperábamos 1)
Al no tener hashCode, cada objeto produce un número distinto, van a cajones distintos y equals ni siquiera llega a llamarse. La regla de oro: si sobrescribes equals, sobrescribe también hashCode, usando los mismos atributos:
@Override
public int hashCode() {
return Objects.hash(codigo);
}
// Ahora carrito.size() da 1
Ya viste cómo escribir equals en herencia. La forma más cómoda de tener ambos correctos es usar un record, que los genera automáticamente:
record Punto(int x, int y) {}
Map<Punto, String> mapa = new HashMap<>();
mapa.put(new Punto(1, 2), "tesoro");
System.out.println(mapa.get(new Punto(1, 2))); // tesoro
Cuidado: no modifiques un objeto que ya está como clave en un
HashMapo dentro de unHashSet. Si cambia suhashCode, quedará en el cajón equivocado y no lo volverás a encontrar. Por eso las claves suelen ser inmutables.
TreeMap y TreeSet no usan hashCode: ordenan con compareTo (interfaz Comparable) o con el Comparator que les pases al crearlos.
El framework de colecciones de un vistazo
| Interfaz | Implementaciones | Duplicados | Orden | Acceso |
|---|---|---|---|---|
List | ArrayList, LinkedList | Sí | Por posición | get(i) |
Set | HashSet, LinkedHashSet, TreeSet | No | Según implementación | contains(x) |
Map | HashMap, LinkedHashMap, TreeMap | Claves no, valores sí | Según implementación | get(clave) |
Cómo elegir:
- ¿Necesitas elementos en orden y acceder por posición?
List. - ¿Te importa solo si algo está o no, sin repetidos?
Set. - ¿Buscas un dato a partir de otro (código → producto)?
Map.
Como con las listas, declara las variables con la interfaz: Map<String, Integer> m = new HashMap<>();.
Errores frecuentes
- Esperar un orden concreto de un
HashMapoHashSet. Si necesitas orden, usaLinkedHash...oTree.... - No comprobar el
nulldeget. UsagetOrDefaultocontainsKey. - Sobrescribir
equalssinhashCode. Los duplicados se cuelan en losHashSety las búsquedas enHashMapfallan. - Usar tipos primitivos:
Map<String, int>no compila; esMap<String, Integer>. - Borrar de un mapa mientras lo recorres con
for-each. LanzaConcurrentModificationException; usamapa.entrySet().removeIf(...). - Olvidar que
retainAllyremoveAllmodifican el conjunto original. Haz una copia antes.
Resumen
| Operación | Código |
|---|---|
| Crear mapa | Map<String, Integer> m = new HashMap<>(); |
| Guardar / leer | m.put(k, v), m.get(k), m.getOrDefault(k, 0) |
| Consultar | m.containsKey(k), m.containsValue(v), m.size() |
| Borrar | m.remove(k) |
| Contar | m.merge(k, 1, Integer::sum) |
| Recorrer | m.entrySet(), m.keySet(), m.values(), m.forEach(...) |
| Crear conjunto | Set<String> s = new HashSet<>(); |
| Quitar duplicados | new LinkedHashSet<>(lista) |
| Unión / intersección / diferencia | addAll, retainAll, removeAll |
| Claves propias | equals + hashCode (o un record) |
En la siguiente lección aprenderás a procesar todas estas colecciones de forma mucho más compacta: lambdas y streams.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.