Las clases de colección en Java se pueden clasificar en Set, List, Map y Queue. Entre estas, Set, List y Queue comparten una interfaz común: Collection.
Por lo tanto, Collection y Map son las interfaces raíz del framework de colecciones de Java. Las colecciones de Java no almacenan realmente los objetos, sino que conservan referencias a los objetos.
Comenzaremos explicando Map, ya que todas las implementaciones de Set se basan internamente en Map. Si se examina la API detalladamente, se puede observar que todos los interfaces y clases de la jerarquía de Set tienen contrapartes en la jerarquía de Map.
Por ejemplo: Set → Map EnumSet → EnumMap SortedSet → SortedMap TreeSet → TreeMap NavigableSet → NavigableMap HashSet → HashMap LinkedHashSet → LinkedHashMap
La diferencia principal es que la jerarquía de Map también incluye clases de implementación adicionales como IdentityHashMap, WeakHashMap, Hashtable y Properties.
Las colecciones Map se utilizan para almacenar datos con relaciones de mapeo, esencialmente funcionando como un array dinámico de tipo Object donde cada elemento es una clase interna Entry de la interfaz Map.
La clase interna Entry encapsula un par clave-valor.
Existe una relación uno a uno unidireccional entre clave y valor, donde una clave específica siempre permite encontrar un valor único y determinado.
Debido a esta característica, las claves deben ser únicas, lo que coincide con las características de Set. De hecho, todas las claves en un Map forman un conjunto Set.
Los valores, en cambio, pueden duplicarse. Solo es posible consultar valores mediante claves, por lo que los valores pueden considerarse dependientes de las claves.
I. HashMap y Hashtable
Hashtable es una clase más antigua que representa una versión segura para hilos de HashMap. Contiene muchos métodos antiguos y se diferencia de HashMap principalmente en dos aspectos:
- HashMap no es seguro para hilos, mientras que Hashtable sí lo es, lo que hace que HashMap tenga mejor rendimiento. Para lograr seguridad en hilos con HashMap, se puede utilizar el método estático synchronizedMap() de la clase de utilidad Collections, que es más simple y práctico.
- Hashtable no permite usar null como clave o valor, lo que provocaría una excepción NullPointerException, mientras que HashMap sí permite valores nulos.
La implementación subyacente de HashMap es un array donde cada elemento es una lista enlazada (pila) de Entry.
Proceso de almacenamiento: Se calcula el índice de almacenamiento en el array basándose en el valor devuelto por hashCode() del elemento. La posición en la lista enlazada de Entry se determina mediante el método equals del elemento.
Si dos elementos tienen el mismo valor hashCode() pero equals() devuelve false, se produce un conflicto de hash. La nueva Entry siempre se coloca en la parte superior de la lista enlazada.
El tamaño predeterminado del array subyacente de HashMap es 16. Estas posiciones de almacenamiento también se conocen como cubos (buckets). El límite de carga predeterminado es 0.75, lo que significa que cuando HashMap está lleno en 3/4, su capacidad se duplica automáticamente. Durante este proceso, los elementos se redistribuyen en nuevos cubos. Este proceso se conoce como rehashing.
public V insertar(K clave, V valor) {
if (clave == null)
return insertarParaClaveNula(valor);
int hash = calcularHash(clave.hashCode());
int i = indicePara(hash, tabla.length);
for (Entrada<K,V> e = tabla[i]; e != null; e = e.siguiente) {
Object k;
if (e.hash == hash && ((k = e.clave) == clave || clave.equals(k))) {
V valorAntiguo = e.valor;
e.valor = valor;
e.registrarAcceso(this);
return valorAntiguo;
}
}
contadorModificaciones++;
agregarEntrada(hash, clave, valor, i);
return null;
}
static int indicePara(int h, int longitud) {
return h & (longitud - 1);
}
Se puede observar que:
- El hash del elemento se calcula a partir del hashCode() de la clave.
- El índice se calcula de manera simplificada a partir del hash del elemento.
Proceso de consulta: Es similar al proceso de almacenamiento. Primero se calcula el índice del array basándose en el valor hashCode(). Si ya hay elementos en esa posición, se compara con todos los elementos de la lista mediante equals(). Si hay una coincidencia, se devuelve el valor.
public V obtener(Object clave) {
if (clave == null)
return obtenerParaClaveNula();
int hash = calcularHash(clave.hashCode());
for (Entrada<K,V> e = tabla[indicePara(hash, tabla.length)];
e != null;
e = e.siguiente) {
Object k;
if (e.hash == hash && ((k = e.clave) == clave || clave.equals(k)))
return e.valor;
}
return null;
}
Todos los atributos clave que participan en el cálculo del hashCode() deben usarse como estándar en la comparcaión del método equals(). No se deberían proporcionar métodos de inyección para los atributos de dependencia de equals() y hashCode(), o se pueden establecer directamente como final.
Proceso de creación: Además del constructor predeterminado, HashMap proporciona constructores que especifican la capacidad inicial y el factor de carga.
public HashMap(int capacidadInicial, float factorCarga) {
if (capacidadInicial < 0)
throw new IllegalArgumentException("Capacidad inicial no válida: " + capacidadInicial);
if (capacidadInicial > CAPACIDAD_MAXIMA)
capacidadInicial = CAPACIDAD_MAXIMA;
if (factorCarga <= 0 || Float.isNaN(factorCarga))
throw new IllegalArgumentException("Factor de carga no válido: " + factorCarga);
// Encontrar una potencia de 2 >= capacidadInicial
int capacidad = 1;
while (capacidad < capacidadInicial)
capacidad <<= 1;
this.factorCarga = factorCarga;
umbral = (int)(capacidad * factorCarga);
tabla = new Entrada[capacidad];
inicializar();
}
Se puede observar que:
- La capacidad inicial máxima es 1 << 30 (no puede ser mayor).
- La capacidad inicial debe ser el menor valor de 2^n que sea mayor o igual a la capacidad especificada.
II. TreeMap
TreeMap utiliza un "árbol rojo-negro" (árbol binario de ordenación) para almacenar cada Entry del Map. Cada Entry es un nodo del árbol rojo-negro.
Todas las Entry siempre se mantienen ordenadas según la clave según una regla especificada.
Un árbol rojo-negro es un árbol binario autoequilibrado donde el valor de cada nodo es mayor o igual que todos los valores de su subárbol izquierdo, y menor o igual que todos los valores de su subárbol derecho.
III. WeakHashMap, IdentityHashMap
En WeakHashMap, cada objeto clave mantiene una referencia débil al objeto real.
La implementación de IdentityHashMap es similar a la de HashMap, pero solo considera que key1 es igual a key2 cuando key1 == key2. No garantiza ningún orden entre los pares clave-valor, ni que este orden permanezca constante con el tiempo.
IV. LinkedHashMap, Properties
LinkedHashMap es una subclase de HashMap que utiliza una lista doblemente enlazada para mantener el orden de inserción de las entradas. Durante la iteración, los elementos se devuelnen en el mismo orden en que se insertaron.
Properties también es una subclase de HashMap que asocia objetos Map con archivos de propiedades, vinculando las claves y valores del Map con los nombres de propiedad y valores del archivo de propiedades.
V. EnumMap
- Todas las claves en EnumMap deben ser valores de enumeración de una clase de enumeración.
- Se implementa internamente como un array, ordenando las claves naturalmente.
- No permite usar null como clave, pero permite null como valor.
- Al crear un EnumMap, debe especificarse una clase de enumeración, asociando el EnumMap con esa clase.
- EnumMap es la implementación de Map con mejor rendimiento.
Nota adicional: El método values() de Map devuelve un objeto de colección que en realidad no contiene ningún objeto Java. Se utiliza principalmente para recorrer todos los valores del mapa.