Roadmap Java - Parte 9: ArrayList e HashMap
- #Java
Este é o nono artigo da série Roadmap Java (antes chamada "Aprendendo Java do Zero"). Recapitulando rapidamente: nas Partes 1 e 2, demos os primeiros passos e montamos o ambiente de desenvolvimento; nas Partes 3, 4 e 5, conhecemos variáveis, tipos, operadores e String; na Parte 6, vimos entrada e saída de dados com Scanner; na Parte 7, aprendemos estruturas condicionais, switch-case, escopo de variável e valores padrão; e na Parte 8, conhecemos Arrays e os quatro formatos de loop da linguagem.
Encerramos o artigo anterior apontando para a principal limitação de um Array: o tamanho é fixo, definido no momento da criação, e não muda depois. Chegou a hora de resolver isso. Neste artigo, nós vamos:
- conhecer a
ArrayList, a alternativa de tamanho dinâmico aos Arrays; - aprender a adicionar, atualizar e remover elementos, um a um ou em lote;
- aprender a remover elementos com base em uma condição, usando expressões lambda;
- aprender a clonar e comparar
ArrayLists; - aprender a verificar se um elemento está presente ou se a lista está vazia;
- aprender a ordenar uma
ArrayList; - aprender a manter apenas os elementos em comum entre duas
ArrayLists; - aprender a executar uma ação sobre todos os elementos de uma
ArrayList, sem precisar de um loop explícito; - conhecer o
HashMap, a estrutura de dados que guarda pares de chave e valor; - repetir, para
HashMap, boa parte do repertório aprendido paraArrayList: inserir e substituir em lote, verificar conteúdo e percorrer todos os elementos.
Como trabalhar com ArrayList em Java
Como vimos na Parte 8, um Array guarda múltiplos valores do mesmo tipo, mas com uma restrição importante: o tamanho é definido na criação e não pode mudar depois. Se você precisar de uma posição a mais do que o previsto, a única saída é criar um Array novo, maior, e copiar todo o conteúdo do antigo para dentro dele.
A classe ArrayList, que faz parte do pacote java.util, resolve exatamente essa limitação: é uma lista de tamanho dinâmico, que cresce e encolhe conforme elementos são adicionados ou removidos, sem que você precise gerenciar esse redimensionamento manualmente.
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numerosImpares = new ArrayList<>();
numerosImpares.add(1);
numerosImpares.add(3);
numerosImpares.add(5);
numerosImpares.add(7);
numerosImpares.add(9);
System.out.println(numerosImpares.toString()); // [1, 3, 5, 7, 9]
}
}
Alguns detalhes de sintaxe merecem atenção logo de cara. O tipo dentro dos sinais de maior e menor (<Integer>) é chamado de tipo genérico, e define qual tipo de dado essa ArrayList específica vai aceitar guardar. Repare que usamos a classe wrapper Integer, e não o tipo primitivo int: uma ArrayList (assim como as demais coleções do Java) só aceita tipos de referência, nunca tipos primitivos diretamente. É exatamente por isso que conhecemos as classes wrapper lá na Parte 3: elas existem, entre outros motivos, para permitir que um tipo primitivo seja usado onde a linguagem exige um objeto. O Java converte 1, 3, 5 e os demais literais para Integer automaticamente nos bastidores, num processo chamado autoboxing, então na prática você não precisa se preocupar em fazer essa conversão manualmente.
Repare também no <> vazio do lado direito, em new ArrayList<>(). Ele é chamado de operador diamante, e permite que o compilador infira o tipo genérico a partir do que já foi declarado do lado esquerdo, sem precisar repetir <Integer> duas vezes na mesma linha.
Diferente de um Array, você não acessa um elemento de uma ArrayList com colchetes. Em vez disso, usa o método get(), passando o índice desejado (que, assim como em Arrays, começa em 0):
System.out.println(numerosImpares.get(2)); // 5
Para atualizar um valor já existente em determinada posição, use set(), passando o índice e o novo valor:
numerosImpares.set(2, 55);
Para descobrir quantos elementos a lista tem no momento, use size(). Diferente de Array, ArrayList não tem a propriedade length:
System.out.println(numerosImpares.size()); // 5
E, para remover um elemento, use remove(), prestando atenção a um detalhe importante: o comportamento desse método muda de acordo com o tipo do argumento passado. Um int primitivo é interpretado como índice; já um objeto (como o resultado de Integer.valueOf(7)) é interpretado como o valor a ser removido:
numerosImpares.remove(Integer.valueOf(7)); // remove o valor 7, não a posição de índice 7
Essa ambiguidade costuma pegar quem está começando de surpresa, principalmente porque numerosImpares.remove(7), sem o Integer.valueOf(), compila normalmente, só que remove pelo índice, não pelo valor. Vale guardar essa distinção como uma pegadinha clássica do dia a dia com ArrayList.
Nota sobre boas práticas: embora este artigo declare as variáveis comoArrayList<Integer>para deixar explícito qual implementação está sendo usada, é comum e recomendado, em código real, declarar do lado esquerdo usando a interfaceListem vez da implementação concreta (List<Integer> numeros = new ArrayList<>();). Isso deixa o código menos dependente de uma implementação específica, facilitando trocar por outra implementação deListno futuro, se necessário. Vamos falar sobre interfaces com mais profundidade mais adiante na série; por enquanto, fica o registro.
Como adicionar ou remover múltiplos elementos
Além de add() e remove(), que operam um elemento por vez, ArrayList também oferece versões em lote, que recebem outra coleção como argumento:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numerosImpares = new ArrayList<>();
numerosImpares.add(1);
numerosImpares.add(3);
numerosImpares.add(5);
ArrayList<Integer> maisNumerosImpares = new ArrayList<>();
maisNumerosImpares.add(7);
maisNumerosImpares.add(9);
maisNumerosImpares.add(11);
numerosImpares.addAll(maisNumerosImpares);
System.out.println(numerosImpares.toString()); // [1, 3, 5, 7, 9, 11]
numerosImpares.removeAll(maisNumerosImpares);
System.out.println(numerosImpares.toString()); // [1, 3, 5]
}
}
addAll() insere, ao final da lista, todos os elementos de outra coleção passada como argumento. removeAll() faz o caminho inverso: remove de numerosImpares todo elemento que também aparecer em maisNumerosImpares, e não apenas o primeiro encontrado. Se o objetivo for esvaziar a lista inteira de uma só vez, sem precisar informar o que remover, existe ainda o método clear().
Como remover elementos com base em uma condição
Até aqui, para remover um elemento específico, foi preciso conhecer seu índice ou seu valor exato de antemão. Mas, muitas vezes, o critério de remoção é uma condição, não um valor fixo: "remova todo número par", por exemplo. Para isso, ArrayList oferece o método removeIf():
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numeros = new ArrayList<>();
for (int i = 0; i <= 10; i++) {
numeros.add(i);
}
numeros.removeIf(numero -> numero % 2 == 1); // remove os ímpares
System.out.println(numeros.toString()); // [0, 2, 4, 6, 8, 10]
}
}
A parte numero -> numero % 2 == 1 é uma expressão lambda: uma forma compacta de escrever uma pequena função anônima, sem precisar declarar um método separado só para isso. De forma bem resumida, o que vem antes da seta (->) é o parâmetro recebido (nesse caso, cada elemento da lista, um de cada vez), e o que vem depois é a expressão avaliada para esse parâmetro, que aqui devolve true ou false. removeIf() percorre a lista inteira e remove todo elemento para o qual essa expressão retornar true. Vamos encontrar expressões lambda mais algumas vezes ao longo deste próprio artigo, e voltaremos a esse assunto com mais profundidade mais adiante na série.
Como clonar e comparar ArrayLists
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numeros = new ArrayList<>();
for (int i = 0; i <= 10; i++) {
numeros.add(i);
}
ArrayList<Integer> numerosClonados = (ArrayList<Integer>) numeros.clone();
System.out.println(numerosClonados.equals(numeros)); // true
}
}
clone() cria uma nova ArrayList, com os mesmos elementos da original, mas devolve o resultado tipado como Object (um tipo bem genérico, que ainda não vimos em detalhe, e que vamos explorar quando chegarmos em classes e objetos). Por isso é preciso um cast explícito de volta para ArrayList<Integer>, seguindo a mesma lógica de conversão explícita de tipos que já vimos na Parte 3.
Para comparar o conteúdo de duas listas, ArrayList já sobrescreve o método equals() para comparar elemento a elemento, na mesma ordem, em vez de comparar se são exatamente o mesmo objeto na memória (que seria o comportamento de ==, como já vimos com String e Arrays nos artigos anteriores). Vale registrar, assim como fizemos naquelas ocasiões: prefira sempre equals() a == quando o que importa é o conteúdo, não a identidade do objeto.
Assim como em Arrays.copyOf(), vale um lembrete: clone() faz uma cópia rasa (shallow copy). Para uma ArrayList<Integer> como a do exemplo, isso não traz problema nenhum na prática, porque Integer é imutável. Mas, para uma ArrayList de objetos mutáveis (algo que vamos ver mais adiante na série), a cópia rasa significa que a lista clonada é independente, porém os objetos dentro dela continuam sendo os mesmos, compartilhados com a lista original.
Como verificar se um elemento está presente ou se a lista está vazia
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numerosImpares = new ArrayList<>();
numerosImpares.add(1);
numerosImpares.add(3);
numerosImpares.add(5);
System.out.println(numerosImpares.isEmpty()); // false
System.out.println(numerosImpares.contains(5)); // true
}
}
isEmpty() devolve true se a lista não tiver nenhum elemento (equivalente, embora mais legível, a checar numerosImpares.size() == 0). contains() verifica se um determinado valor está presente em algum ponto da lista, devolvendo true ou false.
Como ordenar uma ArrayList
import java.util.ArrayList;
import java.util.Comparator;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numeros = new ArrayList<>();
numeros.add(5);
numeros.add(7);
numeros.add(1);
numeros.add(9);
numeros.add(3);
numeros.sort(Comparator.naturalOrder());
System.out.println(numeros.toString()); // [1, 3, 5, 7, 9]
numeros.sort(Comparator.reverseOrder());
System.out.println(numeros.toString()); // [9, 7, 5, 3, 1]
}
}
Diferente de Arrays.sort(), que vimos na Parte 8, o método de ordenação aqui é chamado diretamente sobre o objeto (numeros.sort(...)), e não como um método utilitário separado. Ele também exige um Comparator, que define o critério de ordenação: Comparator.naturalOrder() ordena em ordem crescente (a ordem "natural" do tipo, que para números é a ordem numérica, e para texto é a ordem alfabética), e Comparator.reverseOrder() inverte esse critério, ordenando em ordem decrescente. Assim como Arrays.sort(), ArrayList.sort() também ordena a lista no próprio local, sem criar uma nova lista.
Como manter elementos comuns entre duas ArrayLists
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> lista1 = new ArrayList<>();
lista1.add(1);
lista1.add(3);
lista1.add(5);
ArrayList<Integer> lista2 = new ArrayList<>();
lista2.add(5);
lista2.add(7);
lista2.add(9);
lista1.retainAll(lista2);
System.out.println(lista1.toString()); // [5]
}
}
retainAll() faz, na prática, uma operação equivalente à interseção entre conjuntos: remove de lista1 todo elemento que não existir também em lista2, restando apenas o que é comum às duas. Repare que, assim como removeAll(), esse método modifica a lista sobre a qual foi chamado (lista1, no exemplo); lista2 permanece intacta.
Como executar uma ação em todos os elementos de uma ArrayList
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
ArrayList<Integer> numerosImpares = new ArrayList<>();
numerosImpares.add(1);
numerosImpares.add(3);
numerosImpares.add(5);
numerosImpares.forEach(numero -> {
System.out.printf("%d ", numero * 2); // 2 6 10
});
}
}
forEach() percorre a lista inteira e executa a expressão lambda passada como argumento uma vez para cada elemento, sem que você precise escrever um for ou for-each manualmente para isso. Nesse exemplo específico, a lista original não é alterada: a multiplicação por 2 acontece apenas dentro da lambda, só para fins de impressão, sem ser reatribuída a lugar nenhum. Se o objetivo fosse de fato alterar cada elemento da lista, o caminho seria percorrer por índice com set(), ou recorrer a métodos como replaceAll(), que atualiza cada posição com o resultado de uma função aplicada sobre o valor atual.
Como trabalhar com HashMap em Java
Toda estrutura que vimos até agora (Array, ArrayList) guarda uma sequência de valores, acessados por posição. O HashMap, também do pacote java.util, segue uma lógica diferente: ele guarda dados em pares de chave e valor, e o acesso a cada valor acontece através da sua chave, não de um índice numérico. Se você já teve contato com dicionários em Python ou objetos simples em JavaScript, a ideia é bem parecida.
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, Double> precos = new HashMap<>();
precos.put("maçã", 2.0);
precos.put("laranja", 1.8);
precos.put("goiaba", 1.5);
System.out.println(precos.toString());
}
}
Assim como em ArrayList, os tipos genéricos aqui também precisam ser tipos de referência, nunca tipos primitivos: <String, Double> define que toda chave desse HashMap é uma String, e todo valor é um Double.
put() insere um novo par chave-valor (ou substitui o valor de uma chave que já existisse). Existe também putIfAbsent(), que insere apenas se a chave em questão ainda não existir no mapa, sem sobrescrever um valor já presente.
Para ler um valor a partir da chave, use get():
System.out.println(precos.get("laranja")); // 1.8
Se a chave não existir, get() devolve null, o que pode gerar uma NullPointerException mais adiante no código, caso você tente usar esse null sem checar antes (uma situação parecida com a que vimos ao comparar Strings com null, na Parte 5). Para evitar esse risco, getOrDefault() recebe um segundo argumento, um valor padrão a ser devolvido caso a chave não seja encontrada:
System.out.println(precos.getOrDefault("jaca", 0.0)); // 0.0, "jaca" não existe no mapa
Para atualizar o valor associado a uma chave já existente, use replace(); para remover um par pela chave, use remove(); para saber quantos pares o mapa tem, size(); e para esvaziar tudo de uma vez, clear(), exatamente como já vimos em ArrayList.
Nota sobre ordem: diferente de umaArrayList, umHashMapnão garante nenhuma ordem específica entre seus pares, nem a ordem de inserção, nem a ordem das chaves. Isso é uma consequência direta de como oHashMaporganiza os dados internamente (usando o hash de cada chave para decidir onde guardá-la), e é o motivo pelo qual as operações de busca por chave são tão rápidas. Se a ordem for importante para o seu caso de uso, existeLinkedHashMap, que preserva a ordem de inserção, eTreeMap, que mantém as chaves sempre ordenadas. Ambos fazem parte do mesmo pacotejava.util, e vale a pena conhecê-los quando a ordem entrar em jogo.
Como inserir ou substituir múltiplos elementos em um HashMap
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, Double> precos = new HashMap<>();
precos.put("maçã", 2.0);
precos.put("laranja", 1.8);
HashMap<String, Double> maisPrecos = new HashMap<>();
maisPrecos.put("goiaba", 1.5);
maisPrecos.put("banana", 1.0);
precos.putAll(maisPrecos);
precos.replaceAll((fruta, preco) -> preco * 2);
System.out.println(precos.toString());
}
}
putAll() insere, de uma só vez, todo o conteúdo de outro HashMap dentro do primeiro (chaves repetidas têm seu valor sobrescrito pelo novo mapa). replaceAll() percorre todos os pares já existentes e recalcula cada valor a partir de uma expressão lambda, que aqui recebe dois parâmetros ao mesmo tempo: a chave (fruta) e o valor atual (preco). No exemplo, o preço de cada fruta é simplesmente dobrado.
Como verificar se um HashMap contém um item ou está vazio
System.out.println(precos.containsKey("laranja")); // true, verifica a chave
System.out.println(precos.containsValue(1.8)); // true, verifica o valor
System.out.println(precos.isEmpty()); // false
containsKey() e containsValue() fazem o mesmo tipo de verificação que contains() fazia em ArrayList, só que separadamente para chaves e para valores, já que um HashMap lida com os dois ao mesmo tempo. isEmpty() funciona exatamente como em ArrayList.
Como executar uma ação em todos os elementos de um HashMap
import java.util.HashMap;
public class Main {
public static void main(String[] args) {
HashMap<String, Double> precos = new HashMap<>();
precos.put("maçã", 2.0);
precos.put("laranja", 1.8);
precos.forEach((fruta, preco) -> {
System.out.printf("%s custa %.2f%n", fruta, preco);
});
}
}
Assim como em ArrayList, forEach() percorre todo o conteúdo do mapa e executa a expressão lambda passada como argumento para cada par. A diferença é que a lambda aqui também recebe dois parâmetros (chave e valor), no mesmo formato que já vimos em replaceAll(), na seção anterior.
O que vem a seguir
Neste nono artigo, deixamos para trás a limitação de tamanho fixo dos Arrays: conhecemos a ArrayList, uma lista de tamanho dinâmico, com adição, remoção condicional, clonagem, comparação, ordenação, interseção e execução de ações em lote sobre seus elementos; e conhecemos o HashMap, que guarda dados em pares de chave e valor, repetindo boa parte desse mesmo repertório adaptado ao formato de chave-valor.
Com Arrays, ArrayList e HashMap, você já tem à disposição as principais estruturas de dados do dia a dia em Java para guardar coleções de informação. Mas, até aqui, cada programa desta série girou em torno de um único arquivo, com um único método main. Chegou a hora de dar o próximo grande passo da linguagem: aprender a organizar código em torno de classes e objetos, a base da Programação Orientada a Objetos, um dos pilares que tornaram o Java tão popular em sistemas de grande porte. Na Parte 10 desta série, vamos abrir esse tópico com "Classes e Objetos em Java", cobrindo o que é um método, sobrecarga de métodos, construtores, modificadores de acesso e os métodos getter e setter.
Até lá!
Este artigo é parte do Roadmap Java, uma série de introdução a Java escrita do zero, com progressão didática própria pensada para quem nunca programou na linguagem antes.
Artigos dessa série na DIO:
- Roadmap Java - Parte 1, Pré-requisitos e seu primeiro programa
- Roadmap Java - Parte 2, Instalando o Java, o IntelliJ IDEA e criando seu primeiro projeto local
- Roadmap Java - Parte 3, Variáveis e tipos primitivos
- Roadmap Java - Parte 4, Operadores
- Roadmap Java - Parte 5, Strings
- Roadmap Java - Parte 6: Entrada e Saída de Dados
- Roadmap Java - Parte 7: Estruturas condicionais, switch-case, escopo e valores padrão
- Roadmap Java - Parte 8: Arrays e Loops
- Roadmap Java - Parte 9: ArrayList e HashMap



