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:
-
putretorna o valor anterior para aquela chave, ounullse não havia mapeamento. É assim que você implementa idiomas de "inserir se ausente" — exceto que não precisa, porqueputIfAbsentfaz exatamente isso e é mais claro. -
getretornandonullsignifica 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; usecontainsKeypara desambiguar, ou — melhor ainda — usegetOrDefaultpara 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)masdefem vez denull.putIfAbsent(k, v)—putapenas 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:
| Classe | chave null | valor null |
|---|---|---|
HashMap | uma permitida | muitos permitidos |
LinkedHashMap | uma permitida | muitos permitidos |
TreeMap | não | muitos permitidos |
Hashtable | não | não |
ConcurrentHashMap | não | não |
Map.of(...) (imutável) | não | nã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
| Classe | Estrutura interna | Ordem de iteração | Uso |
|---|---|---|---|
HashMap | tabela hash | não especificada | o padrão |
LinkedHashMap | tabela hash + lista encadeada | ordem de inserção ou acesso | caches LRU, iteração previsível |
TreeMap | árvore rubro-negra | ordenada por chave | consultas de intervalo em chaves, saída ordenada |
Hashtable | tabela hash, sincronizada | não especificada | legado; raramente a escolha certa |
ConcurrentHashMap | tabela hash com striping | não especificada | código multi-thread |
EnumMap | indexado por bit-array | ordem do enum | Map<MyEnum, V> |
Map.of(...) | imutável | não especificada | maps 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.
O que extrair da execução:
merge(word, 1, Integer::sum)é a contagem de palavras moderna e idiomática. Semget/put/verificação denullem lugar algum.computeIfAbsentcria a lista vazia exatamente uma vez por chave — uma forma limpa de construir umMap<K, List<V>>sem espalharif (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. getOrDefaultelimina o motivo mais comum para verificar null. Use-o sempre que houver um valor padrão sensato.- Um
HashMape umTreeMapcom as mesmas entradas sãoequalsentre 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.