W3docs

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étodoO 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:

  1. Calcula h = hashCode(key). Mistura os 16 bits superiores e inferiores — h ^ (h >>> 16) — para que um hash como 0x12340000 não descarte seus bits superiores ao ser mascarado.
  2. Máscara: i = h & (table.length - 1). Isso é h mod length para tamanhos potência de dois, e é mais rápido que o operador módulo.
  3. Percorre a cadeia em table[i]. Se existe um nó com chave igual, sobrescreve seu valor e retorna o antigo.
  4. Caso contrário, insere (ou, desde o Java 8, acrescenta ao final) um novo nó.
  5. 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 doublings

Ou, 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 true

O 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")); // hit

Ordem 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.

java— editable, runs on the server

O que tirar da execução:

  • merge condensa 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 put do resultado. Note que usa get + put em vez de computeIfAbsent — um computeIfAbsent recursivo muta o mapa enquanto sua própria função de mapeamento ainda está sendo executada, e desde o Java 9 isso lança ConcurrentModificationException. Reserve computeIfAbsent para lookups não recursivos de "carregar ou calcular".
  • A ambiguidade com null é real. get retornou null para 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 UserId fornece equals/hashCode corretos 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.

Prática

Prática
Você vê `m.merge(key, 1, Integer::sum)` em código onde `m` é um `Map<String, Integer>`. O que isso faz?
Você vê `m.merge(key, 1, Integer::sum)` em código onde `m` é um `Map<String, Integer>`. O que isso faz?
Was this page helpful?