W3docs

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:

  • put devuelve el valor previo para esa clave, o null si no había ningún mapeo. Así es como se implementan los idiomas "insertar si ausente" — excepto que no es necesario, porque putIfAbsent hace exactamente eso y es más claro.

  • get retornando null significa bien "la clave no está" o bien "la clave está pero su valor es null." Esa es una ambigüedad si el mapa permite valores null; usa containsKey para desambiguar, o — mejor — usa getOrDefault para 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) pero def en lugar de null.
  • putIfAbsent(k, v)put solo 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:

Claseclave nullvalor null
HashMapuna permitidamuchos permitidos
LinkedHashMapuna permitidamuchos permitidos
TreeMapnomuchos permitidos
Hashtablenono
ConcurrentHashMapnono
Map.of(...) (inmutable)nono

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

ClaseEstructura subyacenteOrden de iteraciónUso
HashMaptabla hashno especificadoel predeterminado
LinkedHashMaptabla hash + lista enlazadaorden de inserción o de accesocachés LRU, iteración predecible
TreeMapárbol rojo-negroordenado por claveconsultas de rango sobre claves, salida ordenada
Hashtabletabla hash, sincronizadano especificadolegado; raramente la opción correcta
ConcurrentHashMaptabla hash segmentadano especificadocódigo multihilo
EnumMapindexado por array de bitsorden del enumMap<MyEnum, V>
Map.of(...)inmutableno especificadomapas 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.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • merge(word, 1, Integer::sum) es el conteo de palabras moderno e idiomático. Sin get/put/verificaciones de null en ningún lado.
  • computeIfAbsent crea la lista vacía exactamente una vez por clave — una forma limpia de construir un Map<K, List<V>> sin esparcir if (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.
  • getOrDefault elimina la razón más común para verificar null. Úsalo siempre que haya un valor predeterminado sensato.
  • Un HashMap y un TreeMap con las mismas entradas son equals entre 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.

Práctica

Práctica
`counts` es un `HashMap<String, Integer>`. ¿Cuál línea es la forma idiomática de incrementar el conteo para 'java', tratando una clave ausente como si comenzara desde cero?
`counts` es un `HashMap<String, Integer>`. ¿Cuál línea es la forma idiomática de incrementar el conteo para 'java', tratando una clave ausente como si comenzara desde cero?
Was this page helpful?