Java Hashtable
La clase Hashtable sincronizada en Java, por qué fue reemplazada por HashMap y ConcurrentHashMap, y cuándo aparece.
Hashtable<K, V> es el mapa basado en hash original de Java, que data del JDK 1.0 en 1996 — dos años antes de que se añadiera el framework de colecciones. Cuando Map, HashMap y el resto llegaron en JDK 1.2, Hashtable fue adaptada para implementar Map, pero sus peculiaridades permanecieron: cada método está synchronized, tanto las claves como los valores rechazan null, y la API pública incluye métodos anteriores a las colecciones (elements(), keys()) que preceden a Iterator.
En código nuevo casi nunca lo necesitarás. Este capítulo existe para que reconozcas la clase cuando la veas, entiendas por qué sigue existiendo y sepas qué usar en su lugar.
Por qué sigue existiendo
Tres razones:
- Compatibilidad con versiones anteriores. Un puñado de clases de la biblioteca estándar devuelven
Hashtable—System.getProperties()devuelve una instancia deProperties, que extiendeHashtable<Object, Object>. Algunas APIs antiguas de JNDI (InitialContext(Hashtable)) la aceptan como argumento. - Código existente. Cualquier base de código anterior a aproximadamente 2005 puede tener
Hashtableen lugares donde nadie se molestó en migrar. - Familiaridad equivocada. Aparece en entrevistas y tutoriales, y los principiantes a veces la usan porque "quiero un mapa thread-safe" — sin conocer
ConcurrentHashMap.
Cómo difiere de HashMap
Hashtable y HashMap son ambas tablas hash con encadenamiento, ambas implementan Map<K, V> y ambas tienen O(1) esperado para get/put/remove. Las diferencias:
| Característica | Hashtable | HashMap |
|---|---|---|
| Seguridad en hilos | cada método synchronized sobre toda la tabla | no es thread-safe |
Clave null | rechazada (NullPointerException) | se permite una |
Valor null | rechazado (NullPointerException) | se permiten muchos |
| Orden de iteración | no especificado | no especificado |
| Capacidad por defecto | 11 | 16 |
| Crecimiento de capacidad | 2*old + 1 (tamaños impares, módulo más lento) | se duplica a la siguiente potencia de dos |
| API anterior a colecciones | enumeraciones elements(), keys() | ninguna |
| Conversión a árbol en Java 8 | no | sí — los buckets se convierten en árboles a partir de 8 entradas |
| Iteradores fail-fast | sí, desde el retrofit de 1.2 | sí |
clone() | sí (superficial) | sí (superficial) |
La sincronización es la diferencia principal y la razón más importante por la que Hashtable es lenta: cada lectura y cada escritura adquiere el mismo bloqueo sobre toda la tabla. Un programa multihilo con dos hilos haciendo nada más que get contra un Hashtable es serializado — se turnan para acceder al bloqueo.
Por qué synchronized en cada método no es seguridad real en hilos
Un error sorprendentemente común: los desarrolladores ven "cada método está sincronizado" y piensan que Hashtable hace correcto su código multihilo. No es así. Las operaciones compuestas siguen siendo propensas a condiciones de carrera:
if (!table.containsKey(key)) { // synchronized
table.put(key, computeValue()); // synchronized — but separate lock acquisition
}Entre las dos llamadas, otro hilo puede ejecutar put con la misma clave. Ambos hilos ven que containsKey devuelve false, ambos calculan, ambos ejecutan put. Obtienes dos evaluaciones y el valor incorrecto gana.
La solución actual no es corregir Hashtable sino usar ConcurrentHashMap, que tiene operaciones compuestas atómicas integradas: putIfAbsent, computeIfAbsent, merge, replace(k, old, new). Adquieren los bloqueos correctos internamente y realizan el test-and-set como una sola operación.
Qué usar en su lugar
El flujo de decisión cuando tienes la tentación de escribir new Hashtable<>():
- Código de un solo hilo y quieres un
Map→HashMap. Punto. La sobrecarga desynchronizeddeHashtablees un coste puro sin ningún beneficio. - Código multihilo y quieres un
Map→ConcurrentHashMap. Con bloqueo fragmentado (lock-free para lecturas en JDKs modernos), sin bloqueo global, con operaciones compuestas atómicas y mucha mejor escalabilidad. - Código multihilo y realmente necesitas que cada operación sea atómica con todo lo demás →
Collections.synchronizedMap(new HashMap<>()). Mismo comportamiento de bloqueo único queHashtablepero compatible con la API de colecciones moderna. Sigue siendo peor queConcurrentHashMapsi puedes usarlo. - Ves una API que requiere
Hashtable(Properties, JNDI) → usaHashtableporque la API lo requiere; no introduzcas uno paralelo.
Las peculiaridades anteriores a las colecciones
Hashtable precede a Iterator y expone Enumeration<K> en su lugar:
Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
System.out.println(keys.nextElement());
}Enumeration solo tiene hasMoreElements() y nextElement() — sin remove(). El retrofit de 1.2 añadió keySet(), entrySet() y values() de Map, y puedes iterar sobre ellos con un Iterator normal. Pero como ambas APIs existen en el mismo objeto, verás ambos estilos en la práctica. Prefiere la vista Map; es el lenguaje que ya conoces.
Un ejemplo práctico: Hashtable, por qué rechaza nulls, y la condición de carrera que no protege
El programa siguiente demuestra las diferencias visibles respecto a HashMap — los métodos sincronizados, los nulls rechazados, la enumeración heredada — y muestra la carrera check-then-act que la sincronización de Hashtable no resuelve.
Lo que se puede observar en la ejecución:
- La API básica es la de
Map, por lo queHashtablese parece aHashMappara un uso simple. Resultados idénticos, más lento. - Los nulls son rechazados en ambos lados — es el único
Mapde la familia JDK que rechaza valoresnullademás de clavesnull. - El contador de
Hashtablees incorrecto. Cada método está sincronizado, perogety despuésputson dos operaciones atómicas separadas, no una. Los hilos compiten en medio y pierden actualizaciones. - La versión con
ConcurrentHashMapusandomergees correcta y rápida. Esa es la herramienta adecuada para "mapa thread-safe" en 2026.
Qué sigue
Hashtable tiene un descendiente que sí usarás: Properties, el contenedor de configuración detrás de System.getProperties() y el formato de archivo .properties. Es acotado en alcance y agradable de usar; es el próximo capítulo, y el último capítulo de "estructura de datos" en esta parte del libro antes de avanzar a iteración, ordenamiento y los métodos utilitarios estáticos.