Java HashMap
Numa lista, você encontra as coisas pela posição (o 1º, o 2º…). Mas muitas vezes você quer encontrar pelo nome: o preço do café, a nota da Ana, a capital de Pernambuco. Para isso existe o mapa (Map), que liga cada chave a um valor, e a sua implementação mais usada, o HashMap.
- O que são chave e valor, e como criar um
HashMap. put,get(e onullde uma chave que não existe),getOrDefault,containsKey,remove.- Percorrer um mapa com
keySet,valueseentrySet. - Contar palavras (frequência) de três jeitos, até o
merge. - Por que a ordem não é garantida, e o
LinkedHashMap, que guarda a ordem. - Mapas prontos e imutáveis com
Map.of.
A ideia: um dicionário
Num dicionário de papel, você não procura "a palavra número 3.512": você procura pela palavra e encontra o significado. A palavra é a chave (key); o significado é o valor (value). Um mapa funciona igual:
chave valor "pão" → 0.75 "leite" → 5.49 "café" → 18.90
Duas regras definem um mapa:
- Cada chave aparece uma vez só. Não existem dois "pão" na tabela. Se você guardar "pão" de novo, o valor antigo é substituído.
- Valores podem se repetir. O pão e a banana podem custar o mesmo.
Em outras linguagens isso se chama "dicionário" (Python), "objeto" ou "tabela associativa". No Java, a interface é Map<K, V>: K é o tipo da chave (key) e V, o do valor (value).
Criar, guardar e buscar: put e get
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Double> precos = new HashMap<>();
precos.put("pão", 0.75);
precos.put("leite", 5.49);
precos.put("café", 18.90);
System.out.println(precos);
System.out.println("Tamanho: " + precos.size());
System.out.println("Preço do leite: " + precos.get("leite"));
System.out.println("Preço do queijo: " + precos.get("queijo"));
Double antigo = precos.put("leite", 5.99);
System.out.println("O leite custava " + antigo + ", agora " + precos.get("leite"));
System.out.println(precos);
}
}
- Map<String, Double>
- "Um mapa de
StringparaDouble": a chave é um texto (o produto), o valor é um número decimal (o preço). Lembre: coleções guardam objetos, por issoDoublee nãodouble(veja Wrappers). - new HashMap<>()
- Cria o mapa vazio. A variável é a interface
Map; o objeto, umHashMap(veja programar para a interface). - precos.put("pão", 0.75)
put= colocar. Liga a chave"pão"ao valor0.75. Se a chave já existia, troca o valor e devolve o antigo.- precos.get("leite")
get= pegar. Devolve o valor ligado à chave, ounullse a chave não existe.- precos.size()
- Quantos pares chave-valor existem.
$ javac Main.java
$ java Main
{leite=5.49, pão=0.75, café=18.9} « chaves { }, pares chave=valor
Tamanho: 3
Preço do leite: 5.49
Preço do queijo: null « chave que não existe: null, sem erro
O leite custava 5.49, agora 5.99 « put numa chave existente substitui
{leite=5.99, pão=0.75, café=18.9}
Quatro coisas para reparar:
- O mapa se imprime entre chaves
{ }, com cada par no formatochave=valor. - A ordem não é a da inserção: o leite apareceu antes do pão. Isso tem explicação, e você vai vê-la mais abaixo.
get("queijo")não deu erro: devolveunull, o "nada" do Java. Isso é cômodo, mas perigoso, como mostra o cenário de erro no fim da aula.- O segundo
put("leite", …)não criou outro leite: o mapa continua com 3 pares, e oputdevolveu o preço antigo.
Perguntar e remover
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> estoque = new HashMap<>();
estoque.put("maçã", 12);
estoque.put("banana", 30);
estoque.put("uva", 0);
System.out.println("getOrDefault(\"kiwi\", 0): " + estoque.getOrDefault("kiwi", 0));
System.out.println("getOrDefault(\"maçã\", 0): " + estoque.getOrDefault("maçã", 0));
System.out.println("containsKey(\"uva\"): " + estoque.containsKey("uva"));
System.out.println("containsKey(\"kiwi\"): " + estoque.containsKey("kiwi"));
System.out.println("containsValue(30): " + estoque.containsValue(30));
Integer tirado = estoque.remove("uva");
System.out.println("remove(\"uva\") devolveu " + tirado + ": " + estoque);
System.out.println("remove(\"kiwi\") devolveu " + estoque.remove("kiwi"));
estoque.putIfAbsent("banana", 999);
estoque.putIfAbsent("pera", 7);
System.out.println("depois de putIfAbsent: " + estoque);
}
}
$ javac Main.java
$ java Main
getOrDefault("kiwi", 0): 0 « não tem kiwi: devolveu o padrão
getOrDefault("maçã", 0): 12
containsKey("uva"): true « a uva existe, mesmo com estoque 0
containsKey("kiwi"): false
containsValue(30): true
remove("uva") devolveu 0: {banana=30, maçã=12}
remove("kiwi") devolveu null « não existia: null, sem erro
depois de putIfAbsent: {banana=30, pera=7, maçã=12} « a banana NÃO virou 999
| Método | Tradução | O que faz |
|---|---|---|
getOrDefault(k, padrao) | "pegar ou o padrão" | O valor de k; se k não existe, devolve padrao em vez de null. |
containsKey(k) | "contém a chave" | true se a chave existe. Rápido. |
containsValue(v) | "contém o valor" | true se algum par tem esse valor. Lento: olha todos. |
remove(k) | remover | Tira o par da chave k e devolve o valor que estava lá (ou null). |
putIfAbsent(k, v) | "colocar se ausente" | Só coloca se a chave ainda não existe. |
containsKey ou get(k) != null?
Os dois costumam dar a mesma resposta, mas não sempre: um HashMap aceita null como valor (put("x", null)). Aí get("x") devolve null mesmo com a chave existindo. Para a pergunta "a chave existe?", use containsKey.
Missão: rode e veja kiwi: null. Troque os dois get por getOrDefault com padrão 0, para a saída ser maçã: 12 e kiwi: 0.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> estoque = new HashMap<>();
estoque.put("maçã", 12);
estoque.put("banana", 30);
System.out.println("maçã: " + estoque.get("maçã"));
System.out.println("kiwi: " + estoque.get("kiwi"));
}
}
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> estoque = new HashMap<>();
estoque.put("maçã", 12);
estoque.put("banana", 30);
System.out.println("maçã: " + estoque.getOrDefault("maçã", 0));
System.out.println("kiwi: " + estoque.getOrDefault("kiwi", 0));
}
}
Percorrer um mapa
Um mapa não é uma Collection, então não dá para fazer um for-each direto nele. Mas ele oferece três "visões" que são coleções:
| Visão | Tipo | Contém |
|---|---|---|
keySet() | Set<K> | só as chaves (um conjunto: sem repetidos) |
values() | Collection<V> | só os valores (podem se repetir) |
entrySet() | Set<Map.Entry<K, V>> | os pares; cada par (entry, "entrada") tem getKey() e getValue() |
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Double> notas = new HashMap<>();
notas.put("Ana", 9.5);
notas.put("Bruno", 7.0);
notas.put("Carla", 8.25);
System.out.println("keySet(): " + notas.keySet());
System.out.println("values(): " + notas.values());
System.out.println("entrySet(): " + notas.entrySet());
for (String nome : notas.keySet()) {
System.out.println("Aluno: " + nome);
}
double soma = 0;
for (double n : notas.values()) {
soma += n;
}
System.out.println("Média: " + soma / notas.size());
for (Map.Entry<String, Double> par : notas.entrySet()) {
System.out.println(par.getKey() + " tirou " + par.getValue());
}
notas.forEach((nome, nota) -> System.out.println(nome + " -> " + nota));
}
}
$ javac Main.java $ java Main keySet(): [Bruno, Carla, Ana] values(): [7.0, 8.25, 9.5] entrySet(): [Bruno=7.0, Carla=8.25, Ana=9.5] Aluno: Bruno Aluno: Carla Aluno: Ana Média: 8.25 Bruno tirou 7.0 Carla tirou 8.25 Ana tirou 9.5 Bruno -> 7.0 Carla -> 8.25 Ana -> 9.5
- for (String nome : notas.keySet())
- "Para cada nome no conjunto de chaves". Use quando só precisa das chaves.
- for (double n : notas.values())
- "Para cada valor". Bom para somar, achar o maior… O Java converte cada
Doubleemdoublesozinho. - Map.Entry<String, Double> par
Entryé um tipo que mora "dentro" deMap(por issoMap.Entry). CadapartemgetKey()egetValue().- notas.forEach((nome, nota) -> …)
- A forma curta: uma lambda que recebe a chave e o valor.
Quando precisar da chave e do valor, prefira entrySet() ou forEach: fazer for (String k : keySet()) e chamar get(k) dentro do laço funciona, mas busca cada chave duas vezes.
Contar coisas: a frequência das palavras
O uso mais clássico de um mapa: contar quantas vezes cada coisa aparece. A chave é a coisa (a palavra), o valor é a contagem. Veja três formas, da mais explicada à mais curta. As três dão o mesmo resultado:
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
String frase = "o rato roeu a roupa do rei de roma e o rato fugiu";
String[] palavras = frase.split(" ");
// Forma 1: if com containsKey
Map<String, Integer> f1 = new HashMap<>();
for (String p : palavras) {
if (f1.containsKey(p)) {
f1.put(p, f1.get(p) + 1);
} else {
f1.put(p, 1);
}
}
// Forma 2: getOrDefault
Map<String, Integer> f2 = new HashMap<>();
for (String p : palavras) {
f2.put(p, f2.getOrDefault(p, 0) + 1);
}
// Forma 3: merge
Map<String, Integer> f3 = new HashMap<>();
for (String p : palavras) {
f3.merge(p, 1, Integer::sum);
}
System.out.println(f1);
System.out.println(f2);
System.out.println(f3);
System.out.println("\"rato\" aparece " + f3.get("rato") + " vezes");
}
}
| Forma | Como se lê |
|---|---|
1. if (f1.containsKey(p)) | "Se a palavra já está no mapa, some 1 ao que estava; senão, comece com 1." |
2. f2.getOrDefault(p, 0) + 1 | "Pegue a contagem atual (ou 0, se é a primeira vez) e some 1." |
3. f3.merge(p, 1, Integer::sum) | merge = mesclar. "Se a palavra não existe, guarde 1; se existe, junte o valor velho com o 1 usando a soma." Integer::sum é uma referência de método: o mesmo que (velho, novo) -> velho + novo. |
$ javac Main.java
$ java Main
{fugiu=1, a=1, de=1, roupa=1, roma=1, e=1, rei=1, roeu=1, do=1, o=2, rato=2}
{fugiu=1, a=1, de=1, roupa=1, roma=1, e=1, rei=1, roeu=1, do=1, o=2, rato=2}
{fugiu=1, de=1, a=1, roupa=1, roma=1, e=1, rei=1, roeu=1, do=1, rato=2, o=2}
"rato" aparece 2 vezes
As contagens são iguais nas três: o e rato com 2, o resto com 1. Mas olhe com atenção a terceira linha: de e a trocaram de lugar, e rato e o também. Os mesmos dados, no mesmo tipo de mapa, saíram em outra ordem só porque foram colocados com outro método. É o assunto da próxima seção.
A ordem não é garantida
Por que o HashMap embaralha? Pense num gaveteiro com gavetas numeradas. Para guardar uma chave, o HashMap calcula um número a partir dela (o hash code, uma espécie de "impressão digital" numérica; veja A Classe Object) e usa esse número para escolher a gaveta. Para achar a chave depois, faz a mesma conta e vai direto à gaveta certa, sem olhar as outras. É por isso que o get é tão rápido, mesmo com milhões de chaves.
O preço disso: ao imprimir, o HashMap percorre as gavetas na ordem das gavetas, que não tem nada a ver com a ordem em que você guardou nem com a ordem alfabética. Veja as mesmas sete frutas em três mapas diferentes:
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
String[] frutas = {"uva", "banana", "maçã", "kiwi", "abacaxi", "laranja", "pera"};
Map<String, Integer> hash = new HashMap<>();
Map<String, Integer> linked = new LinkedHashMap<>();
Map<String, Integer> tree = new TreeMap<>();
for (int i = 0; i < frutas.length; i++) {
hash.put(frutas[i], i + 1);
linked.put(frutas[i], i + 1);
tree.put(frutas[i], i + 1);
}
System.out.println("HashMap: " + hash);
System.out.println("LinkedHashMap: " + linked);
System.out.println("TreeMap: " + tree);
}
}
$ javac Main.java
$ java Main
HashMap: {banana=2, pera=7, uva=1, kiwi=4, maçã=3, abacaxi=5, laranja=6} « ordem das "gavetas"
LinkedHashMap: {uva=1, banana=2, maçã=3, kiwi=4, abacaxi=5, laranja=6, pera=7} « ordem de inserção
TreeMap: {abacaxi=5, banana=2, kiwi=4, laranja=6, maçã=3, pera=7, uva=1} « ordem das chaves (A→Z)
O número depois de cada fruta é a ordem em que ela foi colocada. No HashMap saiu 2, 7, 1, 4, 3, 5, 6: nenhuma ordem que faça sentido para nós. E essa ordem pode mudar se o mapa crescer, se os dados forem colocados com outro método (como você viu com o merge) ou numa outra versão do Java.
HashMap
Se o seu programa "funciona" porque o HashMap por acaso imprime na ordem certa, ele pode quebrar amanhã. Quando a ordem importa, escolha o mapa certo:
| Mapa | Ordem ao percorrer | Quando usar |
|---|---|---|
HashMap | Nenhuma garantida | O padrão: você só busca por chave e a ordem não importa. |
LinkedHashMap | A de inserção (quem entrou primeiro sai primeiro) | Um cardápio, um relatório, um formulário: a ordem em que você colocou é a que deve sair. |
TreeMap | Chaves em ordem crescente (alfabética, numérica) | Listagens ordenadas, "a primeira chave depois de X". Veja TreeMap e TreeSet. |
Trocar é fácil: como a variável é do tipo Map, basta mudar o new. O LinkedHashMap funciona exatamente como o HashMap, com os mesmos métodos, só lembrando a ordem; é um pouco mais pesado, porque mantém também uma "corrente" ligando os pares na ordem de chegada.
Mapas prontos: Map.of
Como List.of, o Map.of cria um mapa pronto e imutável, passando chave, valor, chave, valor…
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, String> capitais = Map.of(
"PE", "Recife",
"RN", "Natal",
"PB", "João Pessoa"
);
System.out.println(capitais.get("PE"));
System.out.println(capitais.size());
capitais.put("CE", "Fortaleza");
}
}
$ javac Main.java $ java Main Recife 3 Exception in thread "main" java.lang.UnsupportedOperationException at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:159) at java.base/java.util.ImmutableCollections$AbstractImmutableMap.put(ImmutableCollections.java:1318) at Main.main(Main.java:12)
Ler funciona; put dá UnsupportedOperationException. Para um mapa que pode mudar, copie: new HashMap<>(Map.of(…)). O Map.of aceita até 10 pares; para mais, existe Map.ofEntries(Map.entry("PE", "Recife"), …).
E a ordem? No Map.of ela é embaralhada de propósito, e muda a cada execução, justamente para ninguém depender dela. O mesmo programa rodado três vezes:
Map<String, String> capitais = Map.of("PE", "Recife", "RN", "Natal", "PB", "João Pessoa", "CE", "Fortaleza");
System.out.println(capitais);
$ java Main
{PB=João Pessoa, CE=Fortaleza, RN=Natal, PE=Recife}
$ java Main
{PE=Recife, RN=Natal, CE=Fortaleza, PB=João Pessoa} « outra ordem, mesmo código
$ java Main
{PE=Recife, RN=Natal, CE=Fortaleza, PB=João Pessoa}
Brinque com o código: troque HashMap por LinkedHashMap e depois por TreeMap (lembre do import, ou use java.util.*). Acrescente produtos e veja a ordem de cada um.
import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Double> cardapio = new HashMap<>();
cardapio.put("suco", 6.0);
cardapio.put("café", 4.5);
cardapio.put("pão de queijo", 3.0);
cardapio.put("bolo", 7.25);
for (Map.Entry<String, Double> item : cardapio.entrySet()) {
System.out.println(item.getKey() + ": R$ " + item.getValue());
}
}
}
Quando dá errado
NullPointerException ao guardar o get num int
Este código tem um erro de propósito.
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> idades = new HashMap<>();
idades.put("Ana", 29);
int idade = idades.get("Zeca");
System.out.println(idade);
}
}
$ javac Main.java $ java Main Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null at Main.main(Main.java:8)
O que quer dizer: "não posso chamar Integer.intValue() porque o valor devolvido por Map.get é null". O Zeca não está no mapa, então get devolveu null. Para guardar num int, o Java tenta desembrulhar o Integer (chamando intValue()), e não existe nada para desembrulhar.
Como consertar: use idades.getOrDefault("Zeca", 0), confira antes com containsKey, ou guarde num Integer e teste if (idade != null).
IllegalArgumentException: duplicate key no Map.of
Este código tem um erro de propósito.
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, String> capitais = Map.of("PE", "Recife", "PE", "Olinda");
}
}
$ javac Main.java $ java Main Exception in thread "main" java.lang.IllegalArgumentException: duplicate key: PE at java.base/java.util.ImmutableCollections$MapN.<init>(ImmutableCollections.java:1440) at java.base/java.util.Map.of(Map.java:1384) at Main.main(Main.java:5)
O que quer dizer: "argumento ilegal: chave duplicada: PE". Com put, repetir uma chave substitui o valor em silêncio; o Map.of é mais rigoroso e recusa, porque chave repetida ali é quase sempre erro de digitação.
Como consertar: remova a chave repetida.
O que aconteceu: não é erro: o HashMap não guarda ordem. Veja A ordem não é garantida.
Como consertar: LinkedHashMap para a ordem de inserção; TreeMap para ordem alfabética.
Referência
| Método | O que faz | Devolve |
|---|---|---|
put(k, v) | Liga k a v (substitui se k já existe) | O valor antigo ou null |
get(k) | Busca o valor de k | O valor ou null |
getOrDefault(k, d) | Busca, com padrão | O valor ou d |
containsKey(k) / containsValue(v) | Existe a chave / o valor? | boolean |
remove(k) | Remove o par | O valor removido ou null |
putIfAbsent(k, v) | Coloca só se k não existe | O valor atual ou null |
merge(k, v, função) | Se não existe, guarda v; se existe, combina | O novo valor |
size(), isEmpty(), clear() | Quantidade, vazio?, apagar tudo | — |
keySet(), values(), entrySet() | Visões para percorrer | Set, Collection, Set<Entry> |
forEach((k, v) -> …) | Faz algo com cada par | — |
Map.of(k1, v1, k2, v2…) | Mapa pronto e imutável (até 10 pares) | Map |
new HashMap<>(outro) | Cópia mutável | — |
Exercícios
1. Guardar
Complete para ligar a chave "Ana" ao valor 9.5 no mapa notas:
2. Sem null
Complete para buscar a nota do "Zeca" devolvendo 0.0 se ele não existir:
3. Chave e valor juntos
Complete o tipo do par no for-each:
4. O mapa tem {pão=0.75}. Depois de put("pão", 0.80), quantos pares ele tem?
5. A Ana quer imprimir um cardápio na ordem em que escreveu os itens. Qual mapa?
6. O que get devolve para uma chave que não existe?
Missão final: a turma votou no lanche da festa. Conte os votos num Map<String, Integer> usando merge e mostre pizza: 3, lanche: 2 e sushi: 1 (cada um numa linha, buscando com get).
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
String[] votos = {"pizza", "lanche", "pizza", "sushi", "pizza", "lanche"};
Map<String, Integer> contagem = new HashMap<>();
// conte os votos
// mostre o resultado
}
}
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
String[] votos = {"pizza", "lanche", "pizza", "sushi", "pizza", "lanche"};
Map<String, Integer> contagem = new HashMap<>();
for (String v : votos) {
contagem.merge(v, 1, Integer::sum);
}
System.out.println("pizza: " + contagem.get("pizza"));
System.out.println("lanche: " + contagem.get("lanche"));
System.out.println("sushi: " + contagem.get("sushi"));
}
}
Resumo
- Um
Map<K, V>liga cada chave (única) a um valor.HashMapé a implementação padrão. putguarda (e substitui),getbusca (e devolvenullse não achar),getOrDefaultevita onull.containsKeypergunta,removetira,putIfAbsentsó coloca se não existe.- Percorra com
keySet(),values(),entrySet()ouforEach((k, v) -> …). - Para contar:
mapa.merge(chave, 1, Integer::sum). HashMapnão garante ordem;LinkedHashMapguarda a de inserção;TreeMapordena as chaves.Map.ofcria mapas imutáveis, recusa chaves repetidas e embaralha a ordem a cada execução.int x = mapa.get(chaveQueNaoExiste)dáNullPointerException.
Próxima aula: HashSet, o "mapa só de chaves" para guardar coisas sem repetição.