Java Essencial: Coleções Java (List, Set, Map) e escolha da estrutura adequada

Capítulo 13

Tempo estimado de leitura: 9 minutos

+ Exercício

Visão geral do framework de coleções

O Java Collections Framework é um conjunto de interfaces e classes para armazenar, organizar e manipular grupos de objetos. A escolha da estrutura adequada impacta diretamente legibilidade, corretude (por exemplo, lidar com duplicados) e performance (tempo e memória).

NecessidadeEstrutura recomendadaPor quê
Manter ordem de inserção e acessar por índiceList (ex.: ArrayList)Permite get(i) e mantém sequência
Evitar duplicadosSet (ex.: HashSet)Garante unicidade via equals/hashCode
Chave → valor (dicionário)Map (ex.: HashMap)Busca por chave eficiente
Manter elementos ordenadosTreeSet/TreeMapOrdenação natural ou por Comparator

Interfaces fundamentais: Collection, List, Set e Map

Collection

Collection é a interface base para estruturas que representam “um conjunto de elementos” (com operações como add, remove, contains, size). Ela é estendida por List e Set. Map não estende Collection porque representa pares chave-valor.

List

List representa uma sequência ordenada, permite elementos duplicados e oferece acesso por índice. Use quando a posição importa (ex.: ranking, fila de tarefas, histórico).

  • ArrayList: ótima para leitura e acesso por índice; inserções/remoções no meio podem ser custosas por deslocamento de elementos.
  • LinkedList: boa para muitas inserções/remoções nas extremidades; acesso por índice é mais lento (precisa percorrer nós).

Set

Set representa um conjunto sem duplicados. A noção de “duplicado” depende de equals e, em estruturas baseadas em hash, também de hashCode.

  • HashSet: não garante ordem; operações típicas são muito rápidas (média O(1)).
  • TreeSet: mantém elementos ordenados; operações típicas O(log n); exige ordenação natural (Comparable) ou Comparator.

Map

Map armazena pares chave → valor. Chaves são únicas; valores podem repetir. É ideal para indexação e contagens.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • HashMap: não garante ordem; busca/inserção média O(1).
  • TreeMap: mantém chaves ordenadas; operações O(log n); útil quando você precisa percorrer em ordem.

Generics na prática: evitando casts e erros em runtime

Generics permitem declarar o tipo dos elementos armazenados, garantindo checagem em tempo de compilação e eliminando a necessidade de casts.

Exemplo: List sem generics (evite)

import java.util.*; class Demo { public static void main(String[] args) { List lista = new ArrayList(); lista.add("Ana"); lista.add(10); // mistura tipos, compila List<String> nomes = lista; // warning e pode quebrar em runtime } }

Exemplo: List com generics (recomendado)

import java.util.*; class Demo { public static void main(String[] args) { List<String> nomes = new ArrayList<>(); nomes.add("Ana"); // nomes.add(10); // erro de compilação } }

O mesmo vale para Set<T> e Map<K, V>. Em Map, declare sempre os dois tipos: Map<String, Integer>, por exemplo.

Escolhendo a implementação: critérios práticos

ArrayList vs LinkedList

  • Se você faz muitas leituras (get) e iterações: ArrayList tende a ser melhor.
  • Se você insere/remove frequentemente no início (ou mantém uma fila/deque): LinkedList pode ser uma opção, mas muitas vezes ArrayList ainda é suficiente; avalie com base no padrão real de uso.

HashSet vs TreeSet

  • Se você só precisa de unicidade e rapidez: HashSet.
  • Se precisa manter ordenado (ex.: exibir em ordem alfabética): TreeSet.

HashMap vs TreeMap

  • Se a prioridade é performance de acesso por chave: HashMap.
  • Se você precisa iterar pelas chaves em ordem: TreeMap.

Iteração: for-each e Iterator (e quando usar cada um)

for-each (mais simples e legível)

import java.util.*; class Demo { public static void main(String[] args) { List<String> nomes = List.of("Ana", "Bia", "Caio"); for (String n : nomes) { System.out.println(n); } } }

Use for-each quando você não precisa remover elementos durante a iteração e não precisa do índice.

Iterator (necessário para remover com segurança)

Remover elementos de uma coleção enquanto itera com for-each costuma causar ConcurrentModificationException. Para remoção durante a iteração, use Iterator e iterator.remove().

import java.util.*; class Demo { public static void main(String[] args) { List<String> nomes = new ArrayList<>(List.of("Ana", "", "Bia", "")); Iterator<String> it = nomes.iterator(); while (it.hasNext()) { String atual = it.next(); if (atual.isBlank()) { it.remove(); } } System.out.println(nomes); } }

Iterando Map: entrySet é o caminho mais eficiente

import java.util.*; class Demo { public static void main(String[] args) { Map<String, Integer> freq = new HashMap<>(); freq.put("java", 3); freq.put("maven", 1); for (Map.Entry<String, Integer> e : freq.entrySet()) { System.out.println(e.getKey() + " => " + e.getValue()); } } }

entrySet() evita buscas repetidas por chave (como ocorreria com keySet() + get).

Regras de igualdade: equals/hashCode e impacto em Set/Map

HashSet e HashMap dependem de duas regras: (1) se a.equals(b) é true, então a.hashCode() deve ser igual a b.hashCode(); (2) hashCode deve ser estável enquanto o objeto estiver “imutável” do ponto de vista dos campos usados no cálculo.

Problema comum: objeto mutável usado como chave

Se você usa um objeto como chave em HashMap e depois altera um campo que participa de equals/hashCode, a chave pode “sumir” (o mapa não consegue mais encontrá-la no bucket correto).

Exemplo prático com classe de domínio

import java.util.*; class Produto { private final String sku; private final String nome; Produto(String sku, String nome) { this.sku = sku; this.nome = nome; } public String getSku() { return sku; } public String getNome() { return nome; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Produto)) return false; Produto other = (Produto) o; return Objects.equals(this.sku, other.sku); } @Override public int hashCode() { return Objects.hash(sku); } } class Demo { public static void main(String[] args) { Set<Produto> set = new HashSet<>(); set.add(new Produto("SKU-1", "Teclado")); set.add(new Produto("SKU-1", "Teclado Gamer")); // considerado duplicado pelo sku System.out.println(set.size()); } }

Neste exemplo, a unicidade é definida por sku. Isso é intencional: escolha quais campos definem identidade e mantenha consistência.

Ordenação e comparação: TreeSet/TreeMap e Comparator

TreeSet e TreeMap ordenam elementos/chaves. Você pode usar a ordem natural (implementando Comparable) ou fornecer um Comparator.

Ordenando Strings por tamanho (Comparator)

import java.util.*; class Demo { public static void main(String[] args) { Set<String> palavras = new TreeSet<>(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder())); palavras.addAll(List.of("java", "jdk", "collections", "map", "set")); System.out.println(palavras); } }

Em TreeSet, dois elementos que o Comparator considere “iguais” (comparação retorna 0) não coexistem. Portanto, o critério de ordenação também afeta a noção de duplicidade.

Passo a passo: remover duplicados, contar frequências, ordenar e filtrar

1) Remover duplicados preservando a ordem de aparição

Quando você quer remover duplicados mas manter a ordem original, uma estratégia simples é usar um Set auxiliar para “marcar” o que já apareceu.

import java.util.*; class Demo { public static void main(String[] args) { List<String> entrada = List.of("java", "maven", "java", "jdk", "maven"); Set<String> vistos = new HashSet<>(); List<String> semDuplicados = new ArrayList<>(); for (String item : entrada) { if (vistos.add(item)) { // add retorna false se já existia semDuplicados.add(item); } } System.out.println(semDuplicados); } }

Alternativa: new LinkedHashSet<>(lista) remove duplicados e preserva ordem, mas você perde a estrutura List (pode converter de volta depois).

2) Contar frequências com Map

Use Map<T, Integer> para contar ocorrências. O método getOrDefault simplifica a lógica.

import java.util.*; class Demo { public static void main(String[] args) { List<String> palavras = List.of("java", "java", "jdk", "maven", "java", "maven"); Map<String, Integer> freq = new HashMap<>(); for (String p : palavras) { int atual = freq.getOrDefault(p, 0); freq.put(p, atual + 1); } System.out.println(freq); } }

Se você precisa do resultado ordenado por chave, troque para TreeMap ao final: Map<String, Integer> ordenado = new TreeMap<>(freq);.

3) Ordenar resultados por frequência (desc) e filtrar

Uma forma prática é transformar entrySet() em uma lista e ordenar com Comparator. Depois, filtre por um critério (por exemplo, frequência mínima).

import java.util.*; class Demo { public static void main(String[] args) { Map<String, Integer> freq = new HashMap<>(); freq.put("java", 3); freq.put("maven", 2); freq.put("jdk", 1); List<Map.Entry<String, Integer>> entradas = new ArrayList<>(freq.entrySet()); entradas.sort(Comparator.<Map.Entry<String, Integer>>comparingInt(Map.Entry::getValue).reversed().thenComparing(Map.Entry::getKey)); int minimo = 2; for (Map.Entry<String, Integer> e : entradas) { if (e.getValue() >= minimo) { System.out.println(e.getKey() + ": " + e.getValue()); } } } }

Exercícios propostos

Exercício 1: remover duplicados

Dada uma List<String> com nomes (com repetição), gere uma nova lista sem duplicados preservando a ordem original. Requisitos: (1) não use estruturas de ordenação; (2) explique por que HashSet ajuda; (3) compare com a alternativa LinkedHashSet.

Exercício 2: contador de palavras

Receba uma lista de palavras (simulando tokens já separados) e produza um Map<String, Integer> com frequências. Depois, imprima as palavras em ordem alfabética. Dica: conte com HashMap e ordene com TreeMap no final.

Exercício 3: top N por frequência

Com o mapa de frequências do exercício anterior, gere uma lista de entradas ordenada por frequência decrescente e, em caso de empate, por chave crescente. Imprima apenas as N primeiras (por exemplo, N=3). Dica: use entrySet(), ArrayList e Comparator.

Exercício 4: filtragem e remoção segura

Dada uma List<String> com possíveis strings vazias ou em branco, remova-as durante a iteração sem causar erro. Requisito: use Iterator e remove().

Exercício 5: equals/hashCode na prática

Crie uma classe Aluno com matricula e nome. Coloque objetos em um HashSet e garanta que alunos com a mesma matrícula sejam considerados duplicados. Requisitos: implemente equals e hashCode com base em matricula e demonstre com um teste simples de tamanho do conjunto.

Agora responda o exercício sobre o conteúdo:

Ao percorrer uma List, você precisa remover com segurança os elementos que estão em branco durante a própria iteração. Qual abordagem é a mais adequada para evitar erros de modificação concorrente?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Remover elementos enquanto itera com for-each tende a causar ConcurrentModificationException. Com Iterator, a remoção via iterator.remove() é a maneira segura e suportada durante a iteração.

Próximo capitúlo

Java Essencial: Entrada e saída básica (console e arquivos) com java.io e java.nio

Arrow Right Icon
Capa do Ebook gratuito Java Essencial: Fundamentos da Linguagem e do Ecossistema (JDK, IDE, Maven)
72%

Java Essencial: Fundamentos da Linguagem e do Ecossistema (JDK, IDE, Maven)

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.