Java HashMap
Use o HashMap baseado em tabela hash para buscas rápidas de chave-valor não ordenadas em Java.
HashMap<K, V> é a implementação padrão de Map no JDK e a estrutura de dados mais utilizada no código de aplicações Java. Ela sustenta o HashSet (que é um HashMap com todos os valores definidos como um único elemento fictício), é o que Collectors.toMap constrói, e é a estrutura por trás de toda "tabela de consulta" que você escreve sem ordenação ou concorrência. As operações são O(1) esperado — um hash, um índice de bucket, uma ou duas verificações de equals — independente do tamanho.
Operações principais
Estes são os métodos que você usa diariamente. Todos têm O(1) esperado.
| Método | O que faz |
|---|---|
put(k, v) | Insere ou sobrescreve; retorna o valor anterior (ou null). |
get(k) | Retorna o valor, ou null se a chave estiver ausente. |
getOrDefault(k, def) | Como get, mas retorna def em vez de null quando não encontra. |
putIfAbsent(k, v) | Define o valor somente se a chave estiver ausente ou mapeada para null. |
merge(k, v, fn) | Combina um valor existente com v via fn — o idioma de contador. |
computeIfAbsent(k, fn) | Computa e armazena um valor quando não encontrado — o idioma de cache. |
remove(k) | Remove a entrada; retorna o valor removido, ou null. |
containsKey(k) | A única forma confiável de distinguir "ausente" de "mapeado para null". |
Itere sobre as entradas (não apenas as chaves, quando você precisa dos valores também) para evitar uma segunda busca por chave:
Map<String, Integer> scores = new HashMap<>();
scores.put("alice", 90);
scores.put("bob", 75);
for (Map.Entry<String, Integer> e : scores.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
// Or the lambda form:
scores.forEach((name, score) -> System.out.println(name + " -> " + score));Como a tabela é organizada
Um HashMap mantém um array de buckets com tamanho potência de dois. Inserir uma entrada faz cinco coisas:
- Calcula
h = hashCode(key). Mistura os 16 bits superiores e inferiores —h ^ (h >>> 16)— para que um hash como0x12340000não descarte seus bits superiores ao ser mascarado. - Máscara:
i = h & (table.length - 1). Isso éh mod lengthpara tamanhos potência de dois, e é mais rápido que o operador módulo. - Percorre a cadeia em
table[i]. Se existe um nó com chave igual, sobrescreve seu valor e retorna o antigo. - Caso contrário, insere (ou, desde o Java 8, acrescenta ao final) um novo nó.
- Se
size > capacity * loadFactor, redimensiona: dobra a tabela e redistribui todas as entradas nos buckets.
Até o Java 7, a cadeia de bucket era uma lista simplesmente encadeada, sem mais. A partir do Java 8, quando uma cadeia atinge oito entradas, o bucket é convertido para uma pequena árvore balanceada (árvore rubro-negra) com chaveamento pelo hash. A busca nesse bucket passa a ser O(log n) em vez de O(n), o que limita o impacto de um ataque de negação de serviço que cria hashes colidentes. Se a árvore encolher para seis ou menos entradas, ela reverte para uma lista. Você não verá isso em código normal — só importa quando seu hashCode é adversarial ou patologicamente ruim.
Capacidade, fator de carga e pré-dimensionamento
Os mesmos controles que o HashSet:
- Capacidade inicial — padrão 16, arredondado para a próxima potência de dois.
- Fator de carga — padrão 0.75. Quando
size > capacity * 0.75, a tabela dobra.
Se você conhece o tamanho com antecedência, pré-dimensione:
Map<String, User> users = new HashMap<>(expectedSize * 4 / 3); // skip the doublingsOu, desde o Java 19, a factory explícita:
Map<String, User> users = HashMap.newHashMap(expectedSize);Essa é a expressão mais clara de intenção — ela calcula a capacidade inicial correta a partir do tamanho alvo, para que a tabela não precise crescer.
Chaves e valores null
HashMap permite uma chave null (armazenada no bucket 0 com hash 0) e qualquer número de valores null. Isso é uma conveniência em relação ao Hashtable (que rejeita ambos), mas gera ambiguidade no significado de get(k) == null:
m.put("key", null);
m.get("key"); // returns null
m.containsKey("key"); // returns trueO custo da desambiguação é real. Prefira não armazenar valores null; use Optional, um sentinela, ou simplesmente não inclua a chave. A factory Map.of(...) do Java 9+ impõe isso automaticamente.
hashCode e equals são o seu contrato
Colocar sua própria classe em um HashMap só funciona se hashCode e equals forem consistentes. As mesmas regras do HashSet:
- Objetos iguais devem ter códigos hash iguais.
- Objetos desiguais podem colidir (é normal, é para isso que os buckets têm cadeias).
- Mutar uma chave após a inserção é comportamento indefinido.
Use um record se puder — ambos os métodos são gerados corretamente. Ou deixe a IDE gerá-los. Nunca escreva hashCode manualmente se puder evitar.
record UserId(String tenant, String localPart) {}
Map<UserId, User> directory = new HashMap<>();
directory.put(new UserId("acme", "alice"), new User(/*...*/));
directory.get(new UserId("acme", "alice")); // hitOrdem de iteração — explicitamente indefinida
HashMap não garante nada sobre a ordem de iteração. A ordem depende do layout dos buckets, que depende do hash, da capacidade e do histórico de redimensionamento — pode mudar entre execuções e entre versões da JVM. Se você depende da ordem, seu código está incorreto; se seus testes dependem da ordem, eles são instáveis.
Se a ordem de iteração importa, use LinkedHashMap para a ordem de inserção ou TreeMap para a ordem ordenada. Ambos são substitutos diretos.
Não é thread-safe
HashMap se corrompe sob mutação concorrente — e historicamente um modo de falha notoriamente grave era um loop infinito durante um redimensionamento concorrente. Não compartilhe um HashMap entre threads. A estrutura correta para código multi-threaded é ConcurrentHashMap (abordada mais adiante na parte de concorrência). Collections.synchronizedMap(new HashMap<>()) existe, mas usa um único lock em cada operação, o que é mais lento e raramente a resposta certa.
Um exemplo prático: contador, tabela de consulta e os idiomas modernos
O programa abaixo usa um HashMap de várias formas: um contador de palavras via merge, um cache de memoização recursiva, uma demonstração de ambiguidade com valor null, a factory newHashMap do Java 19 e um record como chave composta.
O que tirar da execução:
mergecondensa os três passos "get, padrão-ou-adiciona-um, put" em uma única chamada. Use-o sempre que estiver mantendo um contador ou soma por chave.- O cache de Fibonacci transforma uma recursão exponencial em linear: verifica o mapa, recorre quando não encontrado, depois faz
putdo resultado. Note que usaget+putem vez decomputeIfAbsent— umcomputeIfAbsentrecursivo muta o mapa enquanto sua própria função de mapeamento ainda está sendo executada, e desde o Java 9 isso lançaConcurrentModificationException. ReservecomputeIfAbsentpara lookups não recursivos de "carregar ou calcular". - A ambiguidade com null é real.
getretornounullpara uma chave presente e uma chave ausente da mesma forma. A única maneira de distingui-las écontainsKey— ou decidindo que você não armazena nulls. - O pré-dimensionamento com
HashMap.newHashMap(1_000_000)permite que um milhão de inserções termine sem nenhum rehash — a tabela começa na capacidade correta. - O record
UserIdforneceequals/hashCodecorretos gratuitamente. Essa é a forma moderna de compor chaves de hash-map a partir de múltiplos campos.
O que vem a seguir
HashMap não garante ordem de iteração. Se você precisa que a ordem de inserção seja lembrada — por exemplo, ao serializar o mapa para JSON e querer saída estável — a ferramenta certa é LinkedHashMap. Ele também é a base de um cache LRU clássico, que abordamos no mesmo capítulo.