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.

Você vai aprender
  • O que são chave e valor, e como criar um HashMap.
  • put, get (e o null de uma chave que não existe), getOrDefault, containsKey, remove.
  • Percorrer um mapa com keySet, values e entrySet.
  • 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 String para Double": a chave é um texto (o produto), o valor é um número decimal (o preço). Lembre: coleções guardam objetos, por isso Double e não double (veja Wrappers).
new HashMap<>()
Cria o mapa vazio. A variável é a interface Map; o objeto, um HashMap (veja programar para a interface).
precos.put("pão", 0.75)
put = colocar. Liga a chave "pão" ao valor 0.75. Se a chave já existia, troca o valor e devolve o antigo.
precos.get("leite")
get = pegar. Devolve o valor ligado à chave, ou null se 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:

  1. O mapa se imprime entre chaves { }, com cada par no formato chave=valor.
  2. 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.
  3. get("queijo") não deu erro: devolveu null, o "nada" do Java. Isso é cômodo, mas perigoso, como mostra o cenário de erro no fim da aula.
  4. O segundo put("leite", …) não criou outro leite: o mapa continua com 3 pares, e o put devolveu 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étodoTraduçãoO 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)removerTira 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ãoTipoConté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 Double em double sozinho.
Map.Entry<String, Double> par
Entry é um tipo que mora "dentro" de Map (por isso Map.Entry). Cada par tem getKey() e getValue().
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");
    }
}
FormaComo 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.

Nunca escreva um programa que dependa da ordem de um 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:

MapaOrdem ao percorrerQuando usar
HashMapNenhuma garantidaO padrão: você só busca por chave e a ordem não importa.
LinkedHashMapA 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.
TreeMapChaves 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.

A saída saiu "embaralhada"

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étodoO que fazDevolve
put(k, v)Liga k a v (substitui se k já existe)O valor antigo ou null
get(k)Busca o valor de kO valor ou null
getOrDefault(k, d)Busca, com padrãoO valor ou d
containsKey(k) / containsValue(v)Existe a chave / o valor?boolean
remove(k)Remove o parO valor removido ou null
putIfAbsent(k, v)Coloca só se k não existeO valor atual ou null
merge(k, v, função)Se não existe, guarda v; se existe, combinaO novo valor
size(), isEmpty(), clear()Quantidade, vazio?, apagar tudo—
keySet(), values(), entrySet()Visões para percorrerSet, 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:

notas.;

2. Sem null

Complete para buscar a nota do "Zeca" devolvendo 0.0 se ele não existir:

double n = notas.;

3. Chave e valor juntos

Complete o tipo do par no for-each:

for ( par : notas.entrySet()) { … }

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.
  • put guarda (e substitui), get busca (e devolve null se não achar), getOrDefault evita o null.
  • containsKey pergunta, remove tira, putIfAbsent só coloca se não existe.
  • Percorra com keySet(), values(), entrySet() ou forEach((k, v) -> …).
  • Para contar: mapa.merge(chave, 1, Integer::sum).
  • HashMap não garante ordem; LinkedHashMap guarda a de inserção; TreeMap ordena as chaves.
  • Map.of cria 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.