Java
Tutorial Java: Mapas
Java map HashMap: uso y ejemplos. Aprende a usar el objeto HashMap en Java para mapear datos con ejemplos prácticos.
Aprende Java y certifícateHashMap: funcionamiento interno y rendimiento
El HashMap es una de las implementaciones más utilizadas de la interfaz Map
en Java y proporciona una estructura de datos que almacena pares clave-valor. Su funcionamiento se basa en el concepto de tabla hash, una estructura que permite operaciones de búsqueda, inserción y eliminación en tiempo constante O(1) en el caso promedio.
Estructura interna
Internamente, un HashMap se compone de un array de "buckets" o cubetas, donde cada una puede contener múltiples entradas. Cuando se añade un par clave-valor, se calcula el código hash de la clave para determinar en qué posición del array debe almacenarse.
Map<String, Integer> puntuaciones = new HashMap<>();
puntuaciones.put("Ana", 95); // Se calcula hash de "Ana" para determinar posición
El proceso interno que sigue un HashMap para almacenar y recuperar valores consta de varios pasos:
1. Se calcula el código hash de la clave mediante el método hashCode()
2. Se aplica una función de dispersión para convertir ese hash en un índice dentro del array
3. Se almacena el par clave-valor en la posición correspondiente
Resolución de colisiones
Las colisiones ocurren cuando dos claves diferentes generan el mismo índice en la tabla hash. Java resuelve este problema mediante el uso de listas enlazadas (hasta Java 8) o árboles binarios (desde Java 8 cuando hay muchas colisiones) para almacenar múltiples entradas en la misma posición.
// Estas dos claves podrían colisionar (aunque es poco probable)
puntuaciones.put("Carlos", 87);
puntuaciones.put("Diana", 92);
A partir de Java 8, cuando una cubeta contiene más de 8 entradas (umbral TREEIFY_THRESHOLD), la lista enlazada se convierte automáticamente en un árbol rojo-negro para mejorar el rendimiento de búsqueda de O(n) a O(log n) en caso de muchas colisiones.
Factor de carga y redimensionamiento
El factor de carga es un parámetro que determina cuándo debe crecer la tabla hash. Por defecto, es 0.75, lo que significa que cuando el 75% de las cubetas están ocupadas, el HashMap aumentará su tamaño (normalmente duplicándolo) y redistribuirá todas las entradas.
// Creación con capacidad inicial y factor de carga personalizados
Map<String, Double> precios = new HashMap<>(100, 0.8f);
Este redimensionamiento es una operación costosa, por lo que si se conoce aproximadamente el número de elementos que se almacenarán, se recomienda especificar una capacidad inicial adecuada para evitar redimensionamientos frecuentes.
Rendimiento de operaciones básicas
Las operaciones fundamentales en un HashMap tienen los siguientes rendimientos:
- put(K, V): O(1) en promedio, O(n) en el peor caso (muchas colisiones)
- get(K): O(1) en promedio, O(log n) con árboles en caso de colisiones
- remove(K): O(1) en promedio, similar a get()
- containsKey(K): O(1) en promedio, similar a get()
- containsValue(V): O(n) ya que requiere recorrer todas las entradas
// Operaciones básicas
Map<Integer, String> empleados = new HashMap<>();
// Inserción - O(1) promedio
empleados.put(1001, "Juan Pérez");
// Búsqueda - O(1) promedio
String nombre = empleados.get(1001);
// Comprobación de existencia - O(1) promedio
boolean existe = empleados.containsKey(1001);
// Eliminación - O(1) promedio
empleados.remove(1001);
Iteración sobre un HashMap
Se puede iterar sobre un HashMap de varias formas, cada una con diferentes características de rendimiento:
Map<String, Integer> población = new HashMap<>();
población.put("Madrid", 3223000);
población.put("Barcelona", 1620000);
población.put("Valencia", 791000);
// Iteración sobre entradas (más eficiente)
for (Map.Entry<String, Integer> entrada : población.entrySet()) {
System.out.println(entrada.getKey() + ": " + entrada.getValue());
}
// Iteración sobre claves (requiere búsqueda adicional para valores)
for (String ciudad : población.keySet()) {
System.out.println(ciudad + ": " + población.get(ciudad));
}
// Iteración con forEach y expresión lambda
población.forEach((ciudad, habitantes) ->
System.out.println(ciudad + " tiene " + habitantes + " habitantes"));
La iteración con entrySet()
es más eficiente que usar keySet()
y luego get()
, ya que evita búsquedas adicionales.
Consideraciones de rendimiento
Para obtener el mejor rendimiento de un HashMap, se deben tener en cuenta varios factores:
- Implementación correcta de hashCode() y equals(): Si se utilizan objetos personalizados como claves, es crucial implementar estos métodos correctamente.
public class Producto {
private String código;
private String nombre;
// Constructor y otros métodos...
@Override
public int hashCode() {
return Objects.hash(código);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Producto producto = (Producto) obj;
return Objects.equals(código, producto.código);
}
}
- Capacidad inicial adecuada: Si se conoce aproximadamente el tamaño final, se puede mejorar el rendimiento estableciendo una capacidad inicial apropiada.
- Claves inmutables: Se recomienda usar claves inmutables para evitar comportamientos inesperados, ya que si una clave cambia después de ser insertada, puede no encontrarse posteriormente.
Comparación con otros mapas
El HashMap ofrece el mejor rendimiento general para operaciones de búsqueda, inserción y eliminación cuando no se requiere un orden específico. Sin embargo, tiene algunas limitaciones:
- No mantiene ningún orden de los elementos
- No es sincronizado (no es seguro para múltiples hilos)
- Permite una única clave nula y múltiples valores nulos
// HashMap no sincronizado (no seguro para múltiples hilos)
Map<String, Integer> mapa = new HashMap<>();
// Versión sincronizada (thread-safe pero menos eficiente)
Map<String, Integer> mapaSincronizado = Collections.synchronizedMap(new HashMap<>());
Casos de uso óptimos
El HashMap es ideal para:
- Cachés de datos: Almacenamiento temporal de información con acceso rápido por clave
- Conteo de frecuencias: Registro de ocurrencias de elementos
- Indexación de datos: Acceso rápido a información mediante identificadores únicos
- Eliminación de duplicados: Almacenamiento de elementos únicos con verificación rápida
// Ejemplo: Conteo de frecuencia de palabras
String texto = "Java es un lenguaje de programación Java es versátil";
Map<String, Integer> frecuencias = new HashMap<>();
for (String palabra : texto.split(" ")) {
frecuencias.put(palabra, frecuencias.getOrDefault(palabra, 0) + 1);
}
// Resultado: {Java=2, es=2, un=1, lenguaje=1, de=1, programación=1, versátil=1}
TreeMap: mapas ordenados por clave
El TreeMap es una implementación de la interfaz Map
en Java que mantiene sus elementos ordenados según el orden natural de las claves o mediante un comparador personalizado. A diferencia del HashMap, que prioriza el rendimiento en operaciones básicas, el TreeMap sacrifica algo de velocidad para garantizar un orden predecible de sus elementos.
Estructura interna y funcionamiento
Internamente, el TreeMap se implementa utilizando un árbol rojo-negro, una estructura de datos de árbol binario balanceado que garantiza operaciones de búsqueda, inserción y eliminación en tiempo logarítmico O(log n). Esta estructura permite mantener las claves ordenadas automáticamente.
// Creación de un TreeMap con orden natural
Map<String, Integer> calificaciones = new TreeMap<>();
calificaciones.put("Carlos", 85);
calificaciones.put("Ana", 92);
calificaciones.put("Beatriz", 78);
calificaciones.put("David", 90);
// Al iterar, las claves aparecerán en orden alfabético: Ana, Beatriz, Carlos, David
calificaciones.forEach((nombre, nota) ->
System.out.println(nombre + ": " + nota));
Cuando se añade un nuevo par clave-valor, el TreeMap utiliza la comparación entre claves para determinar la posición exacta donde debe insertarse el elemento, manteniendo así el árbol balanceado y ordenado en todo momento.
Ordenación de claves
El TreeMap ofrece dos mecanismos para ordenar las claves:
- Orden natural: Las claves deben implementar la interfaz
Comparable
- Comparador personalizado: Se proporciona un objeto
Comparator
al crear el TreeMap
// Usando el orden natural (String implementa Comparable)
TreeMap<String, Double> divisas = new TreeMap<>();
divisas.put("USD", 1.0);
divisas.put("EUR", 1.08);
divisas.put("GBP", 1.27);
// Usando un comparador personalizado (orden inverso)
TreeMap<String, Double> divisasInvertidas = new TreeMap<>(Comparator.reverseOrder());
divisasInvertidas.put("USD", 1.0);
divisasInvertidas.put("EUR", 1.08);
divisasInvertidas.put("GBP", 1.27);
Para utilizar clases personalizadas como claves en un TreeMap, estas deben implementar la interfaz Comparable
o se debe proporcionar un Comparator
específico:
public class Producto implements Comparable<Producto> {
private String nombre;
private double precio;
// Constructor y getters
public Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
}
public String getNombre() { return nombre; }
public double getPrecio() { return precio; }
@Override
public int compareTo(Producto otro) {
// Ordenar por nombre
return this.nombre.compareTo(otro.nombre);
}
@Override
public String toString() {
return nombre + " (" + precio + "€)";
}
}
// Usando la implementación Comparable
TreeMap<Producto, Integer> inventario = new TreeMap<>();
inventario.put(new Producto("Laptop", 899.99), 15);
inventario.put(new Producto("Teclado", 49.99), 30);
// Usando un comparador personalizado (ordenar por precio)
TreeMap<Producto, Integer> porPrecio = new TreeMap<>((p1, p2) ->
Double.compare(p1.getPrecio(), p2.getPrecio()));
porPrecio.put(new Producto("Laptop", 899.99), 15);
porPrecio.put(new Producto("Teclado", 49.99), 30);
Métodos específicos de navegación
Una de las principales ventajas del TreeMap es que ofrece métodos de navegación que no están disponibles en otras implementaciones de Map:
TreeMap<Integer, String> empleados = new TreeMap<>();
empleados.put(1001, "Ana García");
empleados.put(1005, "Carlos López");
empleados.put(1010, "Elena Martín");
empleados.put(1015, "David Sánchez");
empleados.put(1020, "Beatriz Ruiz");
// Obtener la entrada con la clave más baja
Map.Entry<Integer, String> primera = empleados.firstEntry(); // 1001=Ana García
// Obtener la entrada con la clave más alta
Map.Entry<Integer, String> última = empleados.lastEntry(); // 1020=Beatriz Ruiz
// Obtener la entrada con la clave inmediatamente inferior a la dada
Map.Entry<Integer, String> anterior = empleados.lowerEntry(1010); // 1005=Carlos López
// Obtener la entrada con la clave inmediatamente superior a la dada
Map.Entry<Integer, String> siguiente = empleados.higherEntry(1010); // 1015=David Sánchez
// Obtener todas las entradas con claves menores que la especificada
SortedMap<Integer, String> subMapa = empleados.headMap(1010); // {1001=Ana García, 1005=Carlos López}
// Obtener todas las entradas con claves mayores o iguales a la especificada
SortedMap<Integer, String> subMapaDesde = empleados.tailMap(1010); // {1010=Elena Martín, 1015=David Sánchez, 1020=Beatriz Ruiz}
// Obtener un rango de entradas (desde inclusivo, hasta exclusivo)
SortedMap<Integer, String> rango = empleados.subMap(1005, 1015); // {1005=Carlos López, 1010=Elena Martín}
Estos métodos de navegación hacen que el TreeMap sea útil para aplicaciones que requieren acceso ordenado a los datos o necesitan encontrar elementos cercanos a un valor dado.
Rendimiento comparado con HashMap
El TreeMap ofrece operaciones con un rendimiento predecible pero menos eficiente que HashMap:
- put(K, V): O(log n) vs O(1) en HashMap
- get(K): O(log n) vs O(1) en HashMap
- remove(K): O(log n) vs O(1) en HashMap
- containsKey(K): O(log n) vs O(1) en HashMap
// Medición simple de rendimiento
Map<Integer, String> hashMap = new HashMap<>();
Map<Integer, String> treeMap = new TreeMap<>();
long inicio, fin;
// Inserción en HashMap
inicio = System.nanoTime();
for (int i = 0; i < 100000; i++) {
hashMap.put(i, "Valor " + i);
}
fin = System.nanoTime();
System.out.println("Tiempo de inserción en HashMap: " + (fin - inicio) / 1000000 + " ms");
// Inserción en TreeMap
inicio = System.nanoTime();
for (int i = 0; i < 100000; i++) {
treeMap.put(i, "Valor " + i);
}
fin = System.nanoTime();
System.out.println("Tiempo de inserción en TreeMap: " + (fin - inicio) / 1000000 + " ms");
La diferencia de rendimiento se hace más notable a medida que aumenta el tamaño del mapa, pero para colecciones pequeñas o medianas, el impacto puede ser aceptable si se necesita la funcionalidad de ordenación.
Casos de uso óptimos
El TreeMap es la elección ideal cuando se necesita:
- Datos ordenados: Mantener elementos ordenados por clave
- Búsqueda por rango: Encontrar todos los elementos dentro de un rango de claves
- Acceso secuencial: Procesar elementos en orden
- Búsqueda del "vecino más cercano": Encontrar elementos con claves cercanas a un valor dado
// Ejemplo: Sistema de reservas por fecha
TreeMap<LocalDate, List<String>> reservas = new TreeMap<>();
// Añadir algunas reservas
reservas.put(LocalDate.of(2023, 11, 15), List.of("Juan Pérez", "Ana García"));
reservas.put(LocalDate.of(2023, 11, 20), List.of("Carlos López"));
reservas.put(LocalDate.of(2023, 12, 5), List.of("Elena Martín", "David Sánchez"));
reservas.put(LocalDate.of(2023, 12, 10), List.of("Beatriz Ruiz"));
// Obtener todas las reservas de noviembre
LocalDate inicioNoviembre = LocalDate.of(2023, 11, 1);
LocalDate finNoviembre = LocalDate.of(2023, 12, 1);
SortedMap<LocalDate, List<String>> reservasNoviembre = reservas.subMap(inicioNoviembre, finNoviembre);
// Encontrar la próxima fecha disponible después de hoy
LocalDate hoy = LocalDate.now();
Map.Entry<LocalDate, List<String>> proximaReserva = reservas.ceilingEntry(hoy);
Consideraciones importantes
Al trabajar con TreeMap, se deben tener en cuenta algunas consideraciones específicas:
- Consistencia del comparador: El orden definido por el comparador debe ser consistente con equals. Si dos objetos son iguales según equals(), su comparación debe devolver 0.
- Claves inmutables: Al igual que con HashMap, se recomienda usar claves inmutables para evitar comportamientos inesperados.
- Null no permitido en claves: A diferencia de HashMap, TreeMap no permite claves nulas (a menos que se use un comparador que las maneje explícitamente).
// Esto lanzará NullPointerException
TreeMap<String, Integer> mapa = new TreeMap<>();
mapa.put(null, 100); // Error: no se permiten claves nulas
// Esto funciona bien
HashMap<String, Integer> hashMapa = new HashMap<>();
hashMapa.put(null, 100); // Permitido en HashMap
LinkedHashMap: mapas con orden de inserción
El LinkedHashMap representa una implementación híbrida dentro del framework Collections de Java que combina las características de un HashMap con una lista doblemente enlazada. Esta estructura mantiene un registro del orden de inserción de los elementos, ofreciendo un equilibrio entre la eficiencia de acceso del HashMap y la capacidad de preservar el orden de los elementos.
Map<String, Integer> puntuaciones = new LinkedHashMap<>();
puntuaciones.put("Ana", 95);
puntuaciones.put("Carlos", 87);
puntuaciones.put("Beatriz", 92);
// Al iterar, los elementos aparecerán en el orden de inserción: Ana, Carlos, Beatriz
puntuaciones.forEach((nombre, puntuación) ->
System.out.println(nombre + ": " + puntuación));
Estructura interna
Internamente, el LinkedHashMap mantiene dos estructuras:
- Una tabla hash similar a la de HashMap para acceso rápido por clave
- Una lista doblemente enlazada que conecta todas las entradas en su orden de inserción
Cuando se añade un nuevo elemento, además de colocarlo en la tabla hash, se añade al final de la lista enlazada. Esta estructura dual permite que las operaciones de búsqueda mantengan la eficiencia O(1) en promedio, mientras que la iteración refleja el orden en que se insertaron los elementos.
Modos de ordenación
El LinkedHashMap ofrece dos modos de ordenación que se pueden configurar al momento de su creación:
// Modo predeterminado: orden de inserción
Map<String, Double> preciosInserción = new LinkedHashMap<>();
// Modo de acceso: orden de acceso más reciente (LRU)
Map<String, Double> preciosAcceso = new LinkedHashMap<>(16, 0.75f, true);
El tercer parámetro del constructor (accessOrder
) determina el comportamiento:
false
(predeterminado): mantiene el orden de insercióntrue
: mantiene el orden de acceso, reordenando los elementos cuando se accede a ellos
Orden de inserción vs orden de acceso
El modo de orden de inserción es útil cuando se necesita mantener la secuencia exacta en que se añadieron los elementos:
Map<String, String> capitales = new LinkedHashMap<>();
capitales.put("España", "Madrid");
capitales.put("Francia", "París");
capitales.put("Italia", "Roma");
// Iteración en orden de inserción: España, Francia, Italia
for (Map.Entry<String, String> entrada : capitales.entrySet()) {
System.out.println(entrada.getKey() + ": " + entrada.getValue());
}
El modo de orden de acceso (también conocido como LRU - Least Recently Used) reordena los elementos cada vez que se accede a ellos mediante get()
o put()
, moviendo el elemento accedido al final de la lista:
// Creación con orden de acceso (LRU)
Map<String, Integer> cacheLRU = new LinkedHashMap<>(16, 0.75f, true);
cacheLRU.put("A", 1);
cacheLRU.put("B", 2);
cacheLRU.put("C", 3);
// Acceso a un elemento
cacheLRU.get("A"); // Esto mueve "A" al final
// Ahora el orden es: B, C, A
cacheLRU.forEach((k, v) -> System.out.println(k + ": " + v));
Implementación de caché LRU
Una de las aplicaciones más comunes del LinkedHashMap en modo de acceso es la implementación de un caché con política LRU (Least Recently Used), que automáticamente descarta los elementos menos utilizados cuando se alcanza un tamaño máximo:
public class CacheLRU<K, V> extends LinkedHashMap<K, V> {
private final int capacidadMaxima;
public CacheLRU(int capacidadMaxima) {
super(16, 0.75f, true); // Orden de acceso activado
this.capacidadMaxima = capacidadMaxima;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// Elimina la entrada más antigua cuando se supera la capacidad
return size() > capacidadMaxima;
}
}
// Uso del caché LRU
CacheLRU<String, byte[]> imagenCache = new CacheLRU<>(100);
imagenCache.put("imagen1.jpg", new byte[1024]);
// Cuando se supere la capacidad, se eliminarán automáticamente
// las imágenes menos accedidas recientemente
El método removeEldestEntry()
se invoca automáticamente después de cada operación put()
o putAll()
, permitiendo decidir si se debe eliminar la entrada más antigua (la que está al principio de la lista enlazada).
Rendimiento y características
El LinkedHashMap mantiene un rendimiento similar al HashMap para las operaciones básicas:
- put(K, V): O(1) en promedio
- get(K): O(1) en promedio
- remove(K): O(1) en promedio
- containsKey(K): O(1) en promedio
Sin embargo, consume algo más de memoria debido a la lista enlazada adicional que mantiene. Cada entrada en un LinkedHashMap requiere referencias adicionales para los nodos anterior y siguiente en la lista.
// Comparación de rendimiento en iteración
Map<Integer, String> hashMap = new HashMap<>();
Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
// Llenamos ambos mapas con los mismos datos
for (int i = 0; i < 1000; i++) {
int clave = (int)(Math.random() * 10000);
String valor = "Valor " + clave;
hashMap.put(clave, valor);
linkedHashMap.put(clave, valor);
}
// Iteración sobre HashMap (orden impredecible)
long inicio = System.nanoTime();
for (Map.Entry<Integer, String> entrada : hashMap.entrySet()) {
// Procesamiento
}
long finHashMap = System.nanoTime();
// Iteración sobre LinkedHashMap (orden predecible)
for (Map.Entry<Integer, String> entrada : linkedHashMap.entrySet()) {
// Procesamiento
}
long finLinkedHashMap = System.nanoTime();
// La iteración en LinkedHashMap puede ser ligeramente más rápida
// debido a que utiliza la lista enlazada en lugar de recorrer la tabla hash
Casos de uso prácticos
El LinkedHashMap es útil en los siguientes escenarios:
- Mantenimiento del orden de inserción: Cuando se necesita preservar el orden en que se añadieron los elementos.
// Registro de eventos en orden cronológico
Map<LocalDateTime, String> registroEventos = new LinkedHashMap<>();
registroEventos.put(LocalDateTime.now(), "Inicio de sesión");
registroEventos.put(LocalDateTime.now().plusMinutes(5), "Consulta de datos");
registroEventos.put(LocalDateTime.now().plusMinutes(10), "Cierre de sesión");
// Los eventos se mostrarán en orden cronológico
- Cachés con política LRU: Para implementar cachés que descartan automáticamente los elementos menos utilizados.
// Caché de resultados de consultas a base de datos
Map<String, List<Object>> resultadosCache = new LinkedHashMap<>(100, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, List<Object>> eldest) {
return size() > 1000; // Limitar a 1000 consultas en caché
}
};
- Menús y configuraciones: Para mantener el orden visual de elementos en interfaces de usuario.
// Menú de opciones que mantiene el orden definido
Map<String, Runnable> opcionesMenu = new LinkedHashMap<>();
opcionesMenu.put("Nuevo", () -> System.out.println("Creando nuevo documento..."));
opcionesMenu.put("Abrir", () -> System.out.println("Abriendo documento..."));
opcionesMenu.put("Guardar", () -> System.out.println("Guardando documento..."));
opcionesMenu.put("Imprimir", () -> System.out.println("Imprimiendo documento..."));
- Historial de navegación: Para mantener un registro ordenado de páginas visitadas.
// Historial de navegación web
LinkedHashMap<String, LocalDateTime> historial = new LinkedHashMap<>();
historial.put("https://www.ejemplo.com/inicio", LocalDateTime.now());
historial.put("https://www.ejemplo.com/productos", LocalDateTime.now().plusMinutes(2));
historial.put("https://www.ejemplo.com/contacto", LocalDateTime.now().plusMinutes(5));
Comparación con otras implementaciones de Map
El LinkedHashMap ocupa una posición intermedia entre HashMap y TreeMap:
- HashMap: Máxima eficiencia, sin garantía de orden
- LinkedHashMap: Buena eficiencia, orden de inserción o acceso
- TreeMap: Menor eficiencia, orden natural o personalizado de las claves
// Ejemplo comparativo de comportamiento
Map<String, Integer> hashMap = new HashMap<>();
Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
Map<String, Integer> treeMap = new TreeMap<>();
// Añadimos elementos en el mismo orden a los tres mapas
String[] claves = {"C", "A", "B"};
for (String clave : claves) {
hashMap.put(clave, clave.hashCode());
linkedHashMap.put(clave, clave.hashCode());
treeMap.put(clave, clave.hashCode());
}
// HashMap: orden impredecible basado en hash
// LinkedHashMap: C, A, B (orden de inserción)
// TreeMap: A, B, C (orden alfabético)
Consideraciones de uso
Al utilizar LinkedHashMap, se deben tener en cuenta algunas consideraciones:
- Mayor consumo de memoria: Requiere más memoria que HashMap debido a la lista enlazada adicional.
- Sincronización: Al igual que HashMap, no es sincronizado por defecto. Para uso concurrente, se debe envolver con
Collections.synchronizedMap()
.
Map<String, Object> mapaSeguro = Collections.synchronizedMap(new LinkedHashMap<>());
- Claves nulas: Permite una única clave nula, al igual que HashMap.
- Valores nulos: Permite múltiples valores nulos.
Patrones comunes de uso y operaciones avanzadas
Los mapas en Java ofrecen una versatilidad que va más allá de las operaciones básicas de inserción y recuperación.
Combinación de mapas
La combinación de mapas es una operación frecuente cuando se trabaja con múltiples fuentes de datos. Se puede realizar de varias formas según los requisitos específicos:
Map<String, Integer> mapaBase = new HashMap<>();
mapaBase.put("A", 1);
mapaBase.put("B", 2);
Map<String, Integer> mapaAdicional = new HashMap<>();
mapaAdicional.put("B", 3);
mapaAdicional.put("C", 4);
// Método 1: putAll (sobrescribe valores existentes)
Map<String, Integer> resultado1 = new HashMap<>(mapaBase);
resultado1.putAll(mapaAdicional);
// resultado1: {A=1, B=3, C=4}
// Método 2: Combinación selectiva con lógica personalizada
Map<String, Integer> resultado2 = new HashMap<>(mapaBase);
mapaAdicional.forEach((clave, valor) ->
resultado2.merge(clave, valor, (valorExistente, nuevoValor) ->
Math.max(valorExistente, nuevoValor)));
// resultado2: {A=1, B=3, C=4}
El método merge()
es particularmente útil para combinar mapas con lógica personalizada, permitiendo decidir cómo manejar las colisiones de claves.
Mapas bidireccionales
En ocasiones se necesita buscar tanto por clave como por valor. Aunque Java no proporciona una implementación estándar de mapa bidireccional, se puede crear fácilmente:
public class MapaBidireccional<K, V> {
private final Map<K, V> adelante = new HashMap<>();
private final Map<V, K> inverso = new HashMap<>();
public void put(K clave, V valor) {
// Eliminar mapeos antiguos si existen
if (adelante.containsKey(clave)) {
V valorAntiguo = adelante.get(clave);
inverso.remove(valorAntiguo);
}
if (inverso.containsKey(valor)) {
K claveAntigua = inverso.get(valor);
adelante.remove(claveAntigua);
}
adelante.put(clave, valor);
inverso.put(valor, clave);
}
public V getByKey(K clave) {
return adelante.get(clave);
}
public K getByValue(V valor) {
return inverso.get(valor);
}
public void remove(K clave) {
if (adelante.containsKey(clave)) {
V valor = adelante.get(clave);
adelante.remove(clave);
inverso.remove(valor);
}
}
public void removeByValue(V valor) {
if (inverso.containsKey(valor)) {
K clave = inverso.get(valor);
inverso.remove(valor);
adelante.remove(clave);
}
}
}
Este patrón es útil en casos como traducciones, mapeos de IDs a nombres, o cualquier situación donde se necesite buscar en ambas direcciones.
Mapas multivalor
A veces se necesita asociar múltiples valores a una misma clave. Este patrón se implementa típicamente usando un mapa cuyo valor es una colección:
// Mapa de estudiantes por curso
Map<String, List<String>> estudiantesPorCurso = new HashMap<>();
// Añadir estudiantes a cursos
public void inscribirEstudiante(String curso, String estudiante) {
estudiantesPorCurso.computeIfAbsent(curso, k -> new ArrayList<>())
.add(estudiante);
}
// Uso
inscribirEstudiante("Programación", "Ana García");
inscribirEstudiante("Programación", "Carlos López");
inscribirEstudiante("Matemáticas", "Ana García");
// Obtener todos los estudiantes de un curso
List<String> estudiantesProgramacion = estudiantesPorCurso.getOrDefault("Programación", Collections.emptyList());
El método computeIfAbsent()
es fundamental en este patrón, ya que crea la lista automáticamente si la clave no existe, evitando comprobaciones manuales de nulidad.
Contadores y acumuladores
Los mapas son ideales para implementar contadores y acumuladores, especialmente con los métodos funcionales introducidos en versiones recientes de Java:
// Contar frecuencia de palabras en un texto
String texto = "el perro persigue al gato y el gato corre";
Map<String, Integer> frecuencias = new HashMap<>();
for (String palabra : texto.split(" ")) {
frecuencias.merge(palabra, 1, Integer::sum);
}
// frecuencias: {perro=1, el=2, persigue=1, al=1, gato=2, y=1, corre=1}
// Acumular valores por categoría
List<Venta> ventas = List.of(
new Venta("Electrónica", 1200.0),
new Venta("Ropa", 450.0),
new Venta("Electrónica", 800.0),
new Venta("Alimentación", 120.0)
);
Map<String, Double> ventasPorCategoria = new HashMap<>();
for (Venta venta : ventas) {
ventasPorCategoria.merge(venta.getCategoria(), venta.getMonto(), Double::sum);
}
// ventasPorCategoria: {Electrónica=2000.0, Ropa=450.0, Alimentación=120.0}
El método merge()
simplifica enormemente este patrón, eliminando la necesidad de verificaciones condicionales explícitas.
Mapas anidados
Para representar estructuras de datos jerárquicas, los mapas anidados son una solución:
// Mapa de provincias -> ciudades -> códigos postales
Map<String, Map<String, List<String>>> geografía = new HashMap<>();
// Añadir un código postal
public void añadirCódigoPostal(String provincia, String ciudad, String códigoPostal) {
geografía.computeIfAbsent(provincia, k -> new HashMap<>())
.computeIfAbsent(ciudad, k -> new ArrayList<>())
.add(códigoPostal);
}
// Uso
añadirCódigoPostal("Madrid", "Madrid", "28001");
añadirCódigoPostal("Madrid", "Madrid", "28002");
añadirCódigoPostal("Madrid", "Alcobendas", "28100");
añadirCódigoPostal("Barcelona", "Barcelona", "08001");
// Obtener todos los códigos postales de una ciudad
List<String> códigosMadrid = geografía.getOrDefault("Madrid", Collections.emptyMap())
.getOrDefault("Madrid", Collections.emptyList());
Este patrón es muy útil para representar relaciones jerárquicas, pero requiere cuidado para evitar excepciones NullPointerException
al navegar por la estructura.
Operaciones atómicas con compute
Los métodos compute()
, computeIfAbsent()
y computeIfPresent()
permiten realizar operaciones atómicas sobre los valores de un mapa:
Map<String, Integer> inventario = new HashMap<>();
inventario.put("Laptop", 5);
inventario.put("Teclado", 10);
// Incrementar stock (seguro incluso si el producto no existe)
String producto = "Laptop";
int cantidadRecibida = 3;
inventario.compute(producto, (k, v) -> (v == null) ? cantidadRecibida : v + cantidadRecibida);
// inventario: {Laptop=8, Teclado=10}
// Decrementar stock con validación
boolean realizarVenta(String producto, int cantidad) {
return inventario.computeIfPresent(producto, (k, stockActual) -> {
if (stockActual >= cantidad) {
return stockActual - cantidad;
}
// Si no hay suficiente stock, devolver el valor actual sin cambios
return stockActual;
}) != null;
}
// Inicializar valores por defecto
inventario.computeIfAbsent("Ratón", k -> 0);
// inventario: {Laptop=8, Teclado=10, Ratón=0}
Estos métodos son útiles en entornos concurrentes, ya que garantizan que la lectura y actualización del valor se realicen como una operación atómica.
Filtrado y transformación de mapas
Aunque los mapas no implementan directamente la interfaz Stream
, se pueden filtrar y transformar utilizando sus vistas:
Map<String, Double> precios = new HashMap<>();
precios.put("Laptop", 899.99);
precios.put("Teclado", 49.99);
precios.put("Ratón", 29.99);
precios.put("Monitor", 299.99);
// Filtrar productos caros (>100€)
Map<String, Double> productosCostosos = precios.entrySet().stream()
.filter(entry -> entry.getValue() > 100.0)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
// productosCostosos: {Laptop=899.99, Monitor=299.99}
// Aplicar descuento del 10% a todos los productos
Map<String, Double> preciosConDescuento = precios.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue() * 0.9
));
// preciosConDescuento: {Laptop=809.99, Teclado=44.99, Ratón=26.99, Monitor=269.99}
Este patrón permite aplicar toda la potencia de la API Stream a los mapas, facilitando operaciones complejas de filtrado y transformación.
Mapas inmutables y vistas no modificables
Para garantizar la integridad de los datos, a menudo se necesitan mapas inmutables o vistas no modificables:
// Crear mapa inmutable
Map<String, Integer> configuración = Map.of(
"maxConexiones", 100,
"tiempoEspera", 30,
"intentosReconexión", 3
);
// Convertir mapa existente en inmutable
Map<String, Double> tasasCambio = new HashMap<>();
tasasCambio.put("USD", 1.0);
tasasCambio.put("EUR", 0.92);
tasasCambio.put("GBP", 0.78);
Map<String, Double> tasasInmutables = Collections.unmodifiableMap(tasasCambio);
// Crear vista no modificable (cambios en el original se reflejan en la vista)
Map<String, Integer> estadísticas = new HashMap<>();
estadísticas.put("visitas", 1000);
estadísticas.put("descargas", 250);
Map<String, Integer> estadísticasVista = Collections.unmodifiableMap(estadísticas);
estadísticas.put("compras", 50); // Se refleja en estadísticasVista
La diferencia clave es que un mapa inmutable no puede cambiar nunca, mientras que una vista no modificable refleja los cambios realizados en el mapa original pero no permite modificaciones directas.
Sincronización y concurrencia
Para escenarios concurrentes, Java ofrece varias opciones:
// Mapa sincronizado (thread-safe pero con bloqueo completo)
Map<String, Object> mapaSincronizado = Collections.synchronizedMap(new HashMap<>());
// ConcurrentHashMap (mejor rendimiento en concurrencia)
Map<String, Object> mapaConcurrente = new ConcurrentHashMap<>();
// Operaciones atómicas en ConcurrentHashMap
ConcurrentHashMap<String, AtomicInteger> contadores = new ConcurrentHashMap<>();
contadores.put("visitas", new AtomicInteger(0));
// Incremento atómico
contadores.get("visitas").incrementAndGet();
// Operación compuesta atómica
contadores.compute("visitas", (k, v) ->
v == null ? new AtomicInteger(1) : new AtomicInteger(v.get() + 1));
ConcurrentHashMap
ofrece mejor rendimiento en escenarios de alta concurrencia al utilizar bloqueos a nivel de segmento en lugar de bloquear todo el mapa.
Expiración y caché avanzado
Para implementar cachés con expiración, se puede extender el patrón LRU de LinkedHashMap:
public class CacheConExpiración<K, V> {
private final Map<K, V> cache;
private final Map<K, Long> tiemposExpiración;
private final long tiempoVidaMs;
public CacheConExpiración(int capacidad, long tiempoVidaMs) {
this.cache = new LinkedHashMap<K, V>(capacidad, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacidad;
}
};
this.tiemposExpiración = new HashMap<>();
this.tiempoVidaMs = tiempoVidaMs;
}
public synchronized V get(K clave) {
limpiarExpirados();
if (!cache.containsKey(clave)) {
return null;
}
Long tiempoExpiracion = tiemposExpiración.get(clave);
if (tiempoExpiracion != null && System.currentTimeMillis() > tiempoExpiracion) {
cache.remove(clave);
tiemposExpiración.remove(clave);
return null;
}
return cache.get(clave);
}
public synchronized void put(K clave, V valor) {
limpiarExpirados();
cache.put(clave, valor);
tiemposExpiración.put(clave, System.currentTimeMillis() + tiempoVidaMs);
}
private void limpiarExpirados() {
long ahora = System.currentTimeMillis();
Iterator<Map.Entry<K, Long>> it = tiemposExpiración.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<K, Long> entrada = it.next();
if (entrada.getValue() < ahora) {
cache.remove(entrada.getKey());
it.remove();
}
}
}
}
Este patrón es esencial para aplicaciones que necesitan gestionar recursos con tiempo de vida limitado, como sesiones de usuario o resultados de consultas costosas.
Mapas como índices invertidos
Los mapas son ideales para implementar índices invertidos, útiles en búsquedas de texto y sistemas de recuperación de información:
// Índice invertido: palabra -> documentos que la contienen
Map<String, Set<Integer>> índice = new HashMap<>();
// Indexar documentos
void indexarDocumento(int idDocumento, String contenido) {
String[] palabras = contenido.toLowerCase().split("\\W+");
for (String palabra : palabras) {
if (!palabra.isEmpty()) {
índice.computeIfAbsent(palabra, k -> new HashSet<>())
.add(idDocumento);
}
}
}
// Buscar documentos que contienen todas las palabras
Set<Integer> buscar(String consulta) {
String[] términos = consulta.toLowerCase().split("\\W+");
if (términos.length == 0 || términos[0].isEmpty()) {
return Collections.emptySet();
}
// Comenzar con los documentos del primer término
Set<Integer> resultado = new HashSet<>(
índice.getOrDefault(términos[0], Collections.emptySet())
);
// Intersectar con los documentos de los demás términos
for (int i = 1; i < términos.length; i++) {
resultado.retainAll(índice.getOrDefault(términos[i], Collections.emptySet()));
if (resultado.isEmpty()) {
break; // Optimización: si no hay resultados, terminar
}
}
return resultado;
}
Este patrón es la base de los motores de búsqueda y sistemas de recuperación de información, permitiendo búsquedas eficientes en grandes volúmenes de texto.
Ejercicios de esta lección Mapas
Evalúa tus conocimientos de esta lección Mapas con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
CRUD en Java de modelo Customer sobre un ArrayList
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
Interfaces
Enumeraciones Enums
API java.nio 2
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
CRUD en Java de modelo Customer sobre un HashMap
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Streams: match
Gestión de errores y excepciones
Datos primitivos
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Api Java.nio 2
Entrada Y Salida (Io)
Api Java.time
Api Java.time
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el funcionamiento interno de
HashMap
- Conocer cómo funciona el cálculo de hash y su impacto en el rendimiento
- Identificar formas de evitar y manejar colisiones
- Implementar HashMaps con un factor de carga personalizado
- Comparar
HashMap
conTreeMap
yLinkedHashMap
- Aprender a optimizar el uso de
HashMap
en aplicaciones Java