Interfaz Map de Java
Mapeos clave-valor en Java con la interfaz Map — put, get, remove, keySet, values, entrySet y métodos Java 8.
Este capítulo cubre el contrato de Map: sus siete métodos principales, las tres vistas que expone para la iteración, los métodos predeterminados de Java 8 que hacen que el código moderno de mapas sea conciso, las reglas de manejo de valores null por implementación, y cómo se comparan los mapas por igualdad. Al final sabrás qué idiomas usar y qué implementación estándar se adapta a cada tarea.
Map<K, V> es la otra mitad del framework de colecciones. A diferencia de la interfaz Collection, no extiende Collection — es una jerarquía separada, porque almacenar claves asociadas a valores es una abstracción diferente a almacenar un conjunto de elementos. Internamente, la mayoría de las implementaciones de Set son simplemente Maps donde se ignora el valor, por lo que Map es en cierto sentido la estructura principal y Set es el hermano más simple.
El contrato es breve: cada clave se asocia con a lo sumo un valor, las claves forman un conjunto (sin claves duplicadas) y los valores son una colección arbitraria (los valores duplicados están permitidos). Lo que varía entre implementaciones es el orden de iteración, el manejo de nulls, los invariantes de ordenación y la seguridad en hilos — pero los siete métodos principales que se muestran a continuación se comportan igual en todos ellos.
Los siete métodos principales
V put(K key, V value); // insert or overwrite; returns previous value or null
V get(Object key); // lookup; returns null if missing
V remove(Object key); // delete; returns previous value or null
boolean containsKey(Object k); // does the key exist (even if value is null)?
boolean containsValue(Object v); // O(n) scan of values
int size();
boolean isEmpty();Algunas sutilezas que vale la pena interiorizar:
-
putdevuelve el valor previo para esa clave, onullsi no había ningún mapeo. Así es como se implementan los idiomas "insertar si ausente" — excepto que no es necesario, porqueputIfAbsenthace exactamente eso y es más claro. -
getretornandonullsignifica bien "la clave no está" o bien "la clave está pero su valor esnull." Esa es una ambigüedad si el mapa permite valores null; usacontainsKeypara desambiguar, o — mejor — usagetOrDefaultpara proporcionar un centinela:int count = counts.getOrDefault("java", 0); // 0 if absent
Las tres vistas
Un Map no es iterable directamente. Para iterar, se le solicita una de tres vistas de su contenido:
Set<K> keys = map.keySet();
Collection<V> values = map.values();
Set<Map.Entry<K, V>> es = map.entrySet();Estas vistas son en vivo — reflejan los cambios en el mapa subyacente, y los cambios realizados a través de la vista se propagan de vuelta. Eliminar una entrada a través de entrySet() la elimina del mapa; iterar sobre keySet() y llamar a iterator.remove() elimina la entrada. No se puede agregar a keySet o values (no hay valor o clave con qué emparejar), pero sí se puede limpiar o eliminar.
La iteración casi siempre usa entrySet() — obtener ambas partes de cada par a la vez es más económico que llamar a get(k) por cada clave:
for (Map.Entry<String, Integer> e : counts.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}O, la forma lambda añadida en Java 8:
counts.forEach((k, v) -> System.out.println(k + " -> " + v));Los métodos predeterminados de Java 8 que realmente importan
Java 8 añadió varios métodos de Map que reciben una función y se comportan de forma atómica. Convierten muchos patrones de tres líneas en líneas únicas:
getOrDefault(k, def)—get(k)perodefen lugar denull.putIfAbsent(k, v)—putsolo si la clave está ausente.computeIfAbsent(k, fn)— calcula atómicamente el valor si está ausente, lo almacena y lo retorna. La piedra angular de "memorizar esta llamada costosa":Map<String, List<Order>> byUser = new HashMap<>(); byUser.computeIfAbsent(order.user(), u -> new ArrayList<>()).add(order);computeIfPresent(k, biFn)— recalcula solo si la clave ya existe. Útil para contadores que deben ignorar claves no vistas.compute(k, biFn)— universal: pasa el valor actual (o null), obtiene el nuevo. Elimina la entrada si la función retorna null.merge(k, v, biFn)— combina un nuevo valor con el existente si lo hay. El contador clásico:for (String w : words) { counts.merge(w, 1, Integer::sum); // first time: stores 1; subsequent: adds }
Estas son las operaciones que hacen que el manejo moderno de mapas en Java sea conciso. Úsalas en lugar de pares get/put.
Claves y valores null
Las reglas dependen de la implementación:
| Clase | clave null | valor null |
|---|---|---|
HashMap | una permitida | muchos permitidos |
LinkedHashMap | una permitida | muchos permitidos |
TreeMap | no | muchos permitidos |
Hashtable | no | no |
ConcurrentHashMap | no | no |
Map.of(...) (inmutable) | no | no |
La regla general para código nuevo: no almacenar nulls en un mapa. Usa Optional, un valor centinela, o simplemente no insertes la entrada. La fábrica Map.of lo impone por ti.
Igualdad entre implementaciones
Dos mapas son equals si sus entrySet()s son iguales — mismas claves, mismos valores, independientemente del orden de iteración o de la implementación. Un HashMap y un TreeMap con los mismos pares clave-valor son iguales. Esa es la misma regla de "igualdad estructural" que sigue Set.
Las implementaciones estándar, de un vistazo
| Clase | Estructura subyacente | Orden de iteración | Uso |
|---|---|---|---|
HashMap | tabla hash | no especificado | el predeterminado |
LinkedHashMap | tabla hash + lista enlazada | orden de inserción o de acceso | cachés LRU, iteración predecible |
TreeMap | árbol rojo-negro | ordenado por clave | consultas de rango sobre claves, salida ordenada |
Hashtable | tabla hash, sincronizada | no especificado | legado; raramente la opción correcta |
ConcurrentHashMap | tabla hash segmentada | no especificado | código multihilo |
EnumMap | indexado por array de bits | orden del enum | Map<MyEnum, V> |
Map.of(...) | inmutable | no especificado | mapas fijos pequeños |
Los próximos capítulos cubren en profundidad las opciones cotidianas: HashMap, LinkedHashMap y TreeMap. ConcurrentHashMap y EnumMap aparecen en partes posteriores.
Un ejemplo práctico: contadores, agrupación y las tres vistas
El programa a continuación muestra los idiomas modernos de map — merge para contar, computeIfAbsent para agrupar, las tres vistas, y la distinción entre getOrDefault y get.
Lo que se puede extraer de la ejecución:
merge(word, 1, Integer::sum)es el conteo de palabras moderno e idiomático. Singet/put/verificaciones denullen ningún lado.computeIfAbsentcrea la lista vacía exactamente una vez por clave — una forma limpia de construir unMap<K, List<V>>sin esparcirif (m.get(k) == null) m.put(k, new ArrayList<>())por todos lados.- Las tres vistas son ventanas en vivo al mismo mapa;
entrySet()es la forma más económica de iterar cuando se necesitan ambas mitades de cada par. getOrDefaultelimina la razón más común para verificar null. Úsalo siempre que haya un valor predeterminado sensato.- Un
HashMapy unTreeMapcon las mismas entradas sonequalsentre sí; lo único que cambia es el orden de iteración.
Qué sigue
La implementación predeterminada — y la que verás en el 90% del código Java — está respaldada por tabla hash. HashMap es el próximo capítulo; cubriremos el array de cubos, la optimización de conversión a árbol de Java 8, y qué hacer cuando tus claves son tus propias clases.