W3docs

Interface Map do Java

Mapeamentos chave-valor em Java com a interface Map — put, get, remove, keySet, values, entrySet.

Este capítulo cobre o contrato Map: seus sete métodos principais, as três visões que ele expõe para iteração, os métodos default do Java 8 que tornam o código de map moderno conciso, regras de tratamento de null por implementação e como os maps são comparados para igualdade. Ao final, você saberá quais idiomas usar e qual implementação padrão se encaixa em cada situação.

Map<K, V> é a outra metade do framework de coleções. Ao contrário da interface Collection, ela não estende Collection — é uma hierarquia separada, pois armazenar chaves associadas a valores é uma abstração diferente de armazenar uma coleção de elementos. Internamente, a maioria das implementações de Set são apenas Maps onde o valor é ignorado, então Map é, em certo sentido, a estrutura primária e Set é a irmã mais simples.

O contrato é curto: cada chave mapeia para no máximo um valor, as chaves formam um conjunto (sem chaves duplicadas) e os valores são uma coleção arbitrária (valores duplicados são permitidos). O que muda entre implementações é a ordem de iteração, o tratamento de null, os invariantes de ordenação e a segurança de thread — mas os sete métodos principais abaixo se comportam da mesma forma em todos eles.

Os sete métodos principais

V put(K key, V value);          // insert or overwrite; returns previous value or null
V get(Object key);              // lookup; returns null if missing
V remove(Object key);           // delete; returns previous value or null
boolean containsKey(Object k);  // does the key exist (even if value is null)?
boolean containsValue(Object v); // O(n) scan of values
int size();
boolean isEmpty();

Algumas sutilezas que valem a pena internalizar:

  • put retorna o valor anterior para aquela chave, ou null se não havia mapeamento. É assim que você implementa idiomas de "inserir se ausente" — exceto que não precisa, porque putIfAbsent faz exatamente isso e é mais claro.

  • get retornando null significa tanto "a chave não está lá" quanto "a chave está lá mas seu valor é null." Isso é uma ambiguidade se seu map permite valores null; use containsKey para desambiguar, ou — melhor ainda — use getOrDefault para fornecer um sentinel:

    int count = counts.getOrDefault("java", 0); // 0 if absent

As três visões

Um Map não é diretamente iterável. Para iterar, você solicita uma das três visões de seu conteúdo:

Set<K>          keys    = map.keySet();
Collection<V>   values  = map.values();
Set<Map.Entry<K, V>> es = map.entrySet();

Essas visões são dinâmicas — refletem mudanças no map subjacente, e mudanças feitas através da visão se propagam de volta. Remover uma entrada via entrySet() a remove do map; iterar keySet() e chamar iterator.remove() remove a entrada. Você não pode fazer add em keySet ou values (não há valor ou chave para parear), mas pode limpar ou remover.

A iteração quase sempre usa entrySet() — obter ambas as partes de cada par de uma vez é mais barato do que chamar get(k) para cada chave:

for (Map.Entry<String, Integer> e : counts.entrySet()) {
  System.out.println(e.getKey() + " -> " + e.getValue());
}

Ou, a forma lambda adicionada no Java 8:

counts.forEach((k, v) -> System.out.println(k + " -> " + v));

Os métodos default do Java 8 que realmente importam

O Java 8 adicionou vários métodos Map que recebem uma função e se comportam atomicamente. Eles transformam muitos padrões de três linhas em one-liners:

  • getOrDefault(k, def)get(k) mas def em vez de null.
  • putIfAbsent(k, v)put apenas se a chave estiver ausente.
  • computeIfAbsent(k, fn) — calcula atomicamente o valor se ausente, armazena e o retorna. A base do padrão "memoize esta chamada cara":
    Map<String, List<Order>> byUser = new HashMap<>();
    byUser.computeIfAbsent(order.user(), u -> new ArrayList<>()).add(order);
  • computeIfPresent(k, biFn) — recalcula somente se a chave já existir. Útil para contadores que devem ignorar chaves não vistas.
  • compute(k, biFn) — universal: passa o valor atual (ou null), recebe o novo. Remove a entrada se a função retornar null.
  • merge(k, v, biFn) — combina um novo valor com o existente, se houver. O contador clássico:
    for (String w : words) {
      counts.merge(w, 1, Integer::sum);   // first time: stores 1; subsequent: adds
    }

Essas são as operações que tornam o tratamento de map moderno em Java conciso. Use-as em vez de pares get/put.

Chaves null e valores null

As regras dependem da implementação:

Classechave nullvalor null
HashMapuma permitidamuitos permitidos
LinkedHashMapuma permitidamuitos permitidos
TreeMapnãomuitos permitidos
Hashtablenãonão
ConcurrentHashMapnãonão
Map.of(...) (imutável)nãonão

A regra geral para código novo: não armazene nulls em um map. Use Optional, um valor sentinel, ou simplesmente não insira a entrada. A fábrica Map.of impõe isso por você.

Igualdade entre implementações

Dois maps são equals se seus entrySet()s forem iguais — mesmas chaves, mesmos valores, independentemente da ordem de iteração ou implementação. Um HashMap e um TreeMap com os mesmos pares chave-valor são comparados como iguais. É a mesma regra de "igualdade estrutural" que Set segue.

As implementações padrão, em resumo

ClasseEstrutura internaOrdem de iteraçãoUso
HashMaptabela hashnão especificadao padrão
LinkedHashMaptabela hash + lista encadeadaordem de inserção ou acessocaches LRU, iteração previsível
TreeMapárvore rubro-negraordenada por chaveconsultas de intervalo em chaves, saída ordenada
Hashtabletabela hash, sincronizadanão especificadalegado; raramente a escolha certa
ConcurrentHashMaptabela hash com stripingnão especificadacódigo multi-thread
EnumMapindexado por bit-arrayordem do enumMap<MyEnum, V>
Map.of(...)imutávelnão especificadamaps fixos pequenos

Os próximos capítulos cobrem as escolhas cotidianas em profundidade: HashMap, LinkedHashMap e TreeMap. ConcurrentHashMap e EnumMap aparecem em partes posteriores.

Um exemplo prático: contadores, agrupamento e as três visões

O programa abaixo mostra os idiomas modernos de map — merge para contar, computeIfAbsent para agrupar, todas as três visões e a distinção entre getOrDefault e get.

java— editable, runs on the server

O que extrair da execução:

  • merge(word, 1, Integer::sum) é a contagem de palavras moderna e idiomática. Sem get/put/verificação de null em lugar algum.
  • computeIfAbsent cria a lista vazia exatamente uma vez por chave — uma forma limpa de construir um Map<K, List<V>> sem espalhar if (m.get(k) == null) m.put(k, new ArrayList<>()) por todo o código.
  • As três visões são janelas dinâmicas no mesmo map; entrySet() é a forma mais barata de iterar quando você precisa de ambas as metades de cada par.
  • getOrDefault elimina o motivo mais comum para verificar null. Use-o sempre que houver um valor padrão sensato.
  • Um HashMap e um TreeMap com as mesmas entradas são equals entre si; a única coisa que muda é a ordem de iteração.

O que vem a seguir

A implementação padrão — e a que você verá em 90% do código Java — é baseada em tabela hash. HashMap é o próximo capítulo; cobriremos o array de buckets, a otimização de treeificação do Java 8 e o que fazer quando suas chaves são suas próprias classes.

Prática

Prática
`counts` é um `HashMap<String, Integer>`. Qual linha é a forma idiomática de incrementar a contagem de `'java'`, tratando uma chave ausente como iniciando do zero?
`counts` é um `HashMap<String, Integer>`. Qual linha é a forma idiomática de incrementar a contagem de `'java'`, tratando uma chave ausente como iniciando do zero?
Was this page helpful?