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étodo | Qué 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:
- Calcular
h = hashCode(key). Mezcla los 16 bits superiores e inferiores —h ^ (h >>> 16)— para que un hash como0x12340000no pierda sus bits superiores al enmascararse. - Enmascarar:
i = h & (table.length - 1). Esto esh mod lengthpara longitudes potencia de dos, y es más rápido que el operador módulo. - Recorrer la cadena en
table[i]. Si existe un nodo con una clave igual, sobreescribe su valor y devuelve el anterior. - En caso contrario, anteponer (o, desde Java 8, agregar al final) un nuevo nodo.
- 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 doublingsO, 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 trueEl 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")); // hitOrden 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.
Qué observar en la ejecución:
mergecolapsa 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 usaget+puten lugar decomputeIfAbsent— uncomputeIfAbsentrecursivo muta el mapa mientras su propia función de mapeo aún se ejecuta, y desde Java 9 eso lanzaConcurrentModificationException. ReservacomputeIfAbsentpara búsquedas no recursivas de "cargar o calcular". - La ambigüedad con null es real.
getdevolviónullpara una clave presente y una clave ausente de la misma forma. La única manera de distinguirlas escontainsKey— 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
UserIdproporcionaequals/hashCodecorrectos 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.