Saltar al contenido
mapas-y-conjuntos.java · devschool

HashMap y HashSet en Java

Lección 22 de 26 · 10 min de lectura · Actualizado el

En esta lección
  1. Qué es un mapa
  2. Operaciones de Map
  3. Recorrer un mapa
  4. HashMap, LinkedHashMap y TreeMap
  5. Qué es un conjunto
  6. Claves propias: equals y hashCode
  7. El framework de colecciones de un vistazo
  8. Errores frecuentes
  9. Resumen

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étodoDevuelve
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(). Recorrer keySet() y hacer get(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}
HashMapLinkedHashMapTreeMap
OrdenNinguno garantizadoEl de inserciónOrdenado por clave
VelocidadLa más rápidaCasi igualAlgo más lenta
Clave nullAdmite unaAdmite unaNo
Úsalo cuando…El orden no importa (lo normal)Quieres conservar el orden en que añadesNecesitas 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:

HashSetLinkedHashSetTreeSet
OrdenNinguno garantizadoEl de inserciónOrdenado
VelocidadLa más rápidaCasi igualAlgo 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:

  1. 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.
  2. 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 HashMap o dentro de un HashSet. Si cambia su hashCode, 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

InterfazImplementacionesDuplicadosOrdenAcceso
ListArrayList, LinkedListSíPor posiciónget(i)
SetHashSet, LinkedHashSet, TreeSetNoSegún implementacióncontains(x)
MapHashMap, LinkedHashMap, TreeMapClaves no, valores síSegún implementaciónget(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 HashMap o HashSet. Si necesitas orden, usa LinkedHash... o Tree....
  • No comprobar el null de get. Usa getOrDefault o containsKey.
  • Sobrescribir equals sin hashCode. Los duplicados se cuelan en los HashSet y las búsquedas en HashMap fallan.
  • Usar tipos primitivos: Map<String, int> no compila; es Map<String, Integer>.
  • Borrar de un mapa mientras lo recorres con for-each. Lanza ConcurrentModificationException; usa mapa.entrySet().removeIf(...).
  • Olvidar que retainAll y removeAll modifican el conjunto original. Haz una copia antes.

Resumen

OperaciónCódigo
Crear mapaMap<String, Integer> m = new HashMap<>();
Guardar / leerm.put(k, v), m.get(k), m.getOrDefault(k, 0)
Consultarm.containsKey(k), m.containsValue(v), m.size()
Borrarm.remove(k)
Contarm.merge(k, 1, Integer::sum)
Recorrerm.entrySet(), m.keySet(), m.values(), m.forEach(...)
Crear conjuntoSet<String> s = new HashSet<>();
Quitar duplicadosnew LinkedHashSet<>(lista)
Unión / intersección / diferenciaaddAll, retainAll, removeAll
Claves propiasequals + 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

[Java] ¿Qué imprime este código?
Map<String, Integer> votos = new HashMap<>();
votos.put("Ana", 3);
votos.put("Luis", 5);
votos.put("Ana", 7);
System.out.println(votos.size() + " " + votos.get("Ana"));

[Java] ¿Qué imprime este código?
Set<Character> letras = new TreeSet<>();
for (char c : "banana".toCharArray()) {
    letras.add(c);
}
System.out.println(letras);

[Java] Has sobrescrito equals en tu clase Producto, pero no hashCode. ¿Qué puede pasar al meter productos en un HashSet?

[Java] Quieres un diccionario de palabras que, al imprimirlo, muestre las claves en orden alfabético. ¿Qué implementación eliges?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.