Java Hashtable
O Hashtable sincronizado legado do Java, por que foi substituído por HashMap e ConcurrentHashMap, e quando ele aparece.
Hashtable<K, V> é o mapa baseado em hash original do Java, datando do JDK 1.0 em 1996 — dois anos antes de o framework de coleções ser adicionado. Quando Map, HashMap e o restante chegaram no JDK 1.2, Hashtable foi adaptado para implementar Map, mas suas peculiaridades permaneceram: todo método é synchronized, tanto chaves quanto valores rejeitam null, e a API pública inclui métodos pré-coleções (elements(), keys()) que antecedem Iterator.
Em código novo, você quase nunca vai querer usá-lo. Este capítulo existe para que você reconheça a classe quando a encontrar, entenda por que ela ainda existe e saiba o que usar em seu lugar.
Por que ainda existe
Três razões:
- Compatibilidade retroativa. Algumas classes da biblioteca padrão retornam
Hashtable—System.getProperties()retorna uma instância deProperties, que estendeHashtable<Object, Object>. Algumas APIs antigas de JNDI (InitialContext(Hashtable)) recebem uma como argumento. - Código existente. Qualquer base de código mais antiga que cerca de 2005 pode ainda ter
Hashtableem lugares onde ninguém quis migrar. - Familiaridade equivocada. Aparece em entrevistas e tutoriais, e iniciantes às vezes recorrem a ela porque "quero um mapa thread-safe" — sem conhecer o
ConcurrentHashMap.
Como difere do HashMap
Hashtable e HashMap são ambos hash tables com encadeamento, ambos implementam Map<K, V>, e ambos são O(1) esperado para get/put/remove. As diferenças:
| Recurso | Hashtable | HashMap |
|---|---|---|
| Segurança de thread | todo método synchronized na tabela inteira | não é thread-safe |
Chave null | rejeitada (NullPointerException) | uma permitida |
Valor null | rejeitado (NullPointerException) | muitos permitidos |
| Ordem de iteração | não especificada | não especificada |
| Capacidade padrão | 11 | 16 |
| Crescimento de capacidade | 2*old + 1 (tamanhos ímpares, módulo mais lento) | dobra para a próxima potência de dois |
| API pré-coleções | enumerações elements(), keys() | nenhuma |
| Conversão em árvore do Java 8 | não | sim — buckets viram árvores após 8 entradas |
| Iteradores fail-fast | sim, desde o retrofit de 1.2 | sim |
clone() | sim (raso) | sim (raso) |
A sincronização é a principal diferença e o maior motivo pelo qual Hashtable é lento: cada leitura e cada escrita adquire o mesmo lock em toda a tabela. Um programa multithread com duas threads fazendo apenas get contra um Hashtable é serializado — elas se revezam no lock.
Por que synchronized em cada método não é segurança de thread real
Um bug surpreendentemente comum: desenvolvedores veem "todo método é synchronized" e acham que Hashtable torna seu código multithread correto. Não torna. Operações compostas ainda têm condição de corrida:
if (!table.containsKey(key)) { // synchronized
table.put(key, computeValue()); // synchronized — but separate lock acquisition
}Entre as duas chamadas, outra thread pode fazer put da mesma chave. Ambas as threads veem containsKey retornar false, ambas computam, ambas fazem put. Você obtém duas avaliações e o valor errado vence.
A correção hoje não é corrigir o Hashtable, mas usar ConcurrentHashMap, que tem operações compostas atômicas embutidas: putIfAbsent, computeIfAbsent, merge, replace(k, old, new). Elas adquirem os locks corretos internamente e fazem o test-and-set como uma única operação.
O que usar em seu lugar
O fluxo de decisão quando você está tentado a escrever new Hashtable<>():
- Código de thread única, você quer um
Map→HashMap. Ponto. O overheadsynchronizeddoHashtableé custo puro sem benefício. - Código multithread, você quer um
Map→ConcurrentHashMap. Lock-striped (sem lock para leituras em JDKs modernos), sem lock global, operações compostas atômicas, escalabilidade muito melhor. - Código multithread, você genuinamente precisa que cada operação seja atômica com todas as outras →
Collections.synchronizedMap(new HashMap<>()). Mesmo comportamento de lock único que oHashtable, mas compatível com a API moderna de coleções. Ainda pior queConcurrentHashMapse você puder usar esse. - Você está vendo uma API que requer
Hashtable(Properties, JNDI) → use oHashtableporque a API exige; não introduza um paralelo.
As peculiaridades pré-coleções
Hashtable é anterior ao Iterator e expõe Enumeration<K> em vez dele:
Enumeration<String> keys = table.keys();
while (keys.hasMoreElements()) {
System.out.println(keys.nextElement());
}Enumeration tem apenas hasMoreElements() e nextElement() — sem remove(). O retrofit de 1.2 adicionou keySet(), entrySet() e values() do Map, e você pode iterar esses com um Iterator normal. Mas como ambas as APIs existem no mesmo objeto, você verá os dois estilos na prática. Prefira a visão Map; é a linguagem que você já conhece.
Um exemplo prático: Hashtable, por que rejeita nulls e a corrida que não protege
O programa abaixo demonstra as diferenças visíveis em relação ao HashMap — os métodos sincronizados, os nulls rejeitados, a enumeração legada — e mostra a corrida check-then-act que a sincronização do Hashtable não resolve.
O que aprender com a execução:
- A API básica é a do
Map, entãoHashtablese parece comHashMappara uso simples. Resultados idênticos, mais lento. - Nulls são rejeitados em ambos os lados — é o único
Mapda família JDK que rejeita valores null além de chaves null. - O contador com
Hashtableestá errado. Cada método é sincronizado, masgete depoisputsão duas operações atômicas separadas, não uma. Threads concorrem no meio e perdem atualizações. - A versão com
ConcurrentHashMapusandomergeestá correta e é rápida. Essa é a ferramenta certa para "mapa thread-safe" em 2026.
O que vem a seguir
Hashtable tem um descendente que você realmente usará: Properties, o contêiner de configuração por trás de System.getProperties() e do formato de arquivo .properties. É restrito em escopo e agradável de usar; é o próximo capítulo, e o último capítulo de "estrutura de dados" nesta parte do livro antes de passarmos para iteração, ordenação e os métodos utilitários estáticos.