W3docs

Java HashMap

Usa HashMap respaldado por tabla hash para búsquedas clave-valor rápidas y desordenadas en Java.

HashMap<K, V> es la implementación predeterminada de Map en el JDK y la estructura de datos más utilizada en código de aplicaciones Java. Respalda a HashSet (que es un HashMap con todos los valores establecidos en un dummy único), es lo que Collectors.toMap construye, y es la estructura detrás de cada "tabla de búsqueda" que escribes cuando no necesitas orden ni concurrencia. Las operaciones son O(1) esperado — un hash, un índice de bucket, una o dos comprobaciones con equals — independientemente del tamaño.

Operaciones principales

Estos son los métodos que usas a diario. Todos son O(1) esperado.

MétodoQué hace
put(k, v)Inserta o sobreescribe; devuelve el valor anterior (o null).
get(k)Devuelve el valor, o null si la clave está ausente.
getOrDefault(k, def)Como get, pero devuelve def en lugar de null cuando no encuentra la clave.
putIfAbsent(k, v)Establece el valor solo si la clave está ausente o mapeada a null.
merge(k, v, fn)Combina un valor existente con v mediante fn — el idioma del contador.
computeIfAbsent(k, fn)Calcula y almacena un valor cuando no se encuentra — el idioma de la caché.
remove(k)Elimina la entrada; devuelve el valor eliminado, o null.
containsKey(k)La única forma fiable de distinguir "ausente" de "mapeado a null".

Itera sobre las entradas (no solo las claves, cuando también necesitas los valores) para evitar una segunda búsqueda por clave:

Map<String, Integer> scores = new HashMap<>();
scores.put("alice", 90);
scores.put("bob", 75);

for (Map.Entry<String, Integer> e : scores.entrySet()) {
    System.out.println(e.getKey() + " -> " + e.getValue());
}

// Or the lambda form:
scores.forEach((name, score) -> System.out.println(name + " -> " + score));

Cómo está organizada la tabla

Un HashMap mantiene un array de buckets con tamaño potencia de dos. Insertar una entrada realiza cinco operaciones:

  1. Calcular h = hashCode(key). Mezcla los 16 bits superiores e inferiores — h ^ (h >>> 16) — para que un hash como 0x12340000 no pierda sus bits superiores al enmascararse.
  2. Enmascarar: i = h & (table.length - 1). Esto es h mod length para longitudes potencia de dos, y es más rápido que el operador módulo.
  3. Recorrer la cadena en table[i]. Si existe un nodo con una clave igual, sobreescribe su valor y devuelve el anterior.
  4. En caso contrario, anteponer (o, desde Java 8, agregar al final) un nuevo nodo.
  5. Si size > capacity * loadFactor, redimensionar: duplicar la tabla y re-distribuir cada entrada en buckets.

Hasta Java 7 la cadena del bucket era una lista enlazada simple. Desde Java 8 en adelante, cuando una cadena alcanza ocho entradas, el bucket se convierte en un pequeño árbol balanceado (árbol rojo-negro) indexado por el hash. La búsqueda en ese bucket pasa a ser O(log n) en lugar de O(n), lo que limita el daño de un ataque de denegación de servicio que genere hashes colisionantes. Si el árbol se reduce a seis entradas o menos, vuelve a ser una lista. No verás esto en código normal — solo importa cuando tu hashCode es adversarial o patológicamente malo.

Capacidad, factor de carga y pre-dimensionado

Los mismos parámetros que HashSet:

  • Capacidad inicial — por defecto 16, redondeada hacia arriba a la potencia de dos más cercana.
  • Factor de carga — por defecto 0.75. Cuando size > capacity * 0.75, la tabla se duplica.

Si conoces el tamaño de antemano, pre-dimensiona:

Map<String, User> users = new HashMap<>(expectedSize * 4 / 3); // skip the doublings

O, desde Java 19, el factory explícito:

Map<String, User> users = HashMap.newHashMap(expectedSize);

Esta es la expresión más clara de la intención — calcula la capacidad inicial correcta a partir de un tamaño objetivo para que la tabla no necesite crecer.

Claves null y valores null

HashMap permite una clave null (se almacena en el bucket 0 con hash 0) y cualquier número de valores null. Esto es una ventaja sobre Hashtable (que rechaza ambos), pero enturbia el significado de get(k) == null:

m.put("key", null);
m.get("key");          // returns null
m.containsKey("key"); // returns true

El costo de la desambiguación es real. Prefiere no almacenar valores null; usa Optional, un centinela, o simplemente omite la clave. El factory de Java 9+ Map.of(...) te lo impone automáticamente.

hashCode y equals son tu contrato

Colocar tu propia clase en un HashMap solo funciona si hashCode y equals son consistentes. Las mismas reglas que para HashSet:

  • Los objetos iguales deben tener códigos hash iguales.
  • Los objetos desiguales pueden colisionar (está bien, por eso los buckets son cadenas).
  • Mutar una clave después de insertarla es comportamiento indefinido.

Usa un record si puedes — ambos métodos se generan correctamente. O deja que el IDE los genere. Nunca escribas hashCode a mano si puedes evitarlo.

record UserId(String tenant, String localPart) {}
Map<UserId, User> directory = new HashMap<>();
directory.put(new UserId("acme", "alice"), new User(/*...*/));
directory.get(new UserId("acme", "alice")); // hit

Orden de iteración — explícitamente indefinido

HashMap no ofrece ninguna garantía sobre el orden de iteración. El orden depende del diseño del bucket, que depende del hash, la capacidad y el historial de redimensionamientos — puede cambiar entre ejecuciones y entre versiones de la JVM. Si dependes del orden, tu código está roto; si tus pruebas dependen del orden, son frágiles.

Si el orden de iteración importa, usa LinkedHashMap para el orden de inserción o TreeMap para el orden ordenado. Ambos son reemplazos directos.

No es seguro para hilos

HashMap se corrompe bajo mutación concurrente — e históricamente un modo de fallo notoriamente grave era un bucle infinito durante un redimensionamiento concurrente. No compartas un HashMap entre hilos. La estructura correcta para código multihilo es ConcurrentHashMap (cubierta más adelante en la parte de concurrencia). Collections.synchronizedMap(new HashMap<>()) existe pero usa un único bloqueo alrededor de cada operación, lo cual es más lento y rara vez la respuesta correcta.

Un ejemplo completo: contador, tabla de búsqueda y los idiomas modernos

El programa a continuación usa un HashMap de varias formas: un contador de palabras mediante merge, una caché de memoización recursiva, una demostración de ambigüedad con valores null, el factory newHashMap de Java 19, y un record como clave compuesta.

java— editable, runs on the server

Qué observar en la ejecución:

  • merge colapsa los tres pasos de "obtener, poner por defecto o sumar uno, guardar" en una sola llamada. Úsalo siempre que mantengas un contador o suma por clave.
  • La caché de Fibonacci convierte una recursión exponencial en lineal: comprueba el mapa, recurre si no encuentra, luego guarda el resultado con put. Nota que usa get + put en lugar de computeIfAbsent — un computeIfAbsent recursivo muta el mapa mientras su propia función de mapeo aún se ejecuta, y desde Java 9 eso lanza ConcurrentModificationException. Reserva computeIfAbsent para búsquedas no recursivas de "cargar o calcular".
  • La ambigüedad con null es real. get devolvió null para una clave presente y una clave ausente de la misma forma. La única manera de distinguirlas es containsKey — o decidiendo que no almacenas nulls en primer lugar.
  • El pre-dimensionado con HashMap.newHashMap(1_000_000) permite que un millón de inserciones terminen sin ningún rehash — la tabla comienza con la capacidad correcta.
  • El record UserId proporciona equals/hashCode correctos de forma gratuita. Esa es la forma moderna de componer claves de hash map a partir de múltiples campos.

Qué sigue

HashMap no garantiza el orden de iteración. Si necesitas recordar el orden de inserción — por ejemplo, al serializar el mapa a JSON y querer una salida estable — la herramienta correcta es LinkedHashMap. También es la base de una caché LRU de libro de texto, que cubrimos en el mismo capítulo.

Práctica

Práctica
Ves `m.merge(key, 1, Integer::sum)` en código donde `m` es un `Map<String, Integer>`. ¿Qué hace?
Ves `m.merge(key, 1, Integer::sum)` en código donde `m` es un `Map<String, Integer>`. ¿Qué hace?
Was this page helpful?