Pular para o conteúdo principal

Capítulo 7: Coleções Java — O guia para entrevistas

Objetivo

Este capítulo cobre tudo o que você precisa saber sobre Coleções Java para uma entrevista: quando usar cada uma, as diferenças entre List.of() e new ArrayList<>(), armadilhas comuns e perguntas clássicas.


1. A hierarquia das Coleções​

                    Iterable
│
Collection
╱ │ ╲
List Set Queue
╱ │ │ ╲ │
ArrayList │ HashSet │ LinkedList
LinkedList │ TreeSet │ PriorityQueue
│ LinkedHashSet
Vector
(legacy)

Map (não é uma Collection!)
╱ │ ╲
HashMap TreeMap LinkedHashMap
│
ConcurrentHashMap

2. List.of() vs new ArrayList — Mutável vs Imutável​

A diferença fundamental​

CriaçãoTipoModificável?Aceita null?
List.of("a", "b")Lista imutávelNãoNão
new ArrayList<>(List.of("a", "b"))Lista mutávelSimSim
Collections.unmodifiableList(list)View imutávelNão (mas a original sim)Sim
List.copyOf(list)Cópia imutávelNãoNão

Exemplos concretos​

// IMUTÁVEL — não pode ser modificada
List<String> immutable = List.of("a", "b", "c");
immutable.add("d"); // 💥 UnsupportedOperationException
immutable.remove(0); // 💥 UnsupportedOperationException
immutable.set(0, "x"); // 💥 UnsupportedOperationException

// MUTÁVEL — pode ser modificada
List<String> mutable = new ArrayList<>();
mutable.add("a"); // ✅ OK
mutable.add("b"); // ✅ OK
mutable.remove(0); // ✅ OK
mutable.set(0, "x"); // ✅ OK

// Criar uma lista mutável a partir de uma imutável
List<String> mutable2 = new ArrayList<>(List.of("a", "b", "c"));
mutable2.add("d"); // ✅ OK

Map.of() e Set.of() — mesmo princípio​

// Map imutável
Map<String, Integer> map = Map.of("a", 1, "b", 2);
map.put("c", 3); // 💥 UnsupportedOperationException

// Map mutável
Map<String, Integer> map2 = new HashMap<>(Map.of("a", 1, "b", 2));
map2.put("c", 3); // ✅ OK

// Set imutável
Set<String> set = Set.of("a", "b", "c");
set.add("d"); // 💥 UnsupportedOperationException

// Set mutável
Set<String> set2 = new HashSet<>(Set.of("a", "b", "c"));
set2.add("d"); // ✅ OK

Collections.emptyList() vs List.of()​

// Ambos retornam uma lista vazia imutável
List<String> empty1 = Collections.emptyList(); // Java 5+
List<String> empty2 = List.of(); // Java 9+ (preferido)

// Ambos são equivalentes, List.of() é mais moderno

Quando usar cada um?​

SituaçãoUsarPor quê
Constantes, configuraçõesList.of()Imutável, thread-safe
Construir uma lista progressivamentenew ArrayList<>()Precisa adicionar elementos
Retorno de método (caso vazio)List.of() ou Collections.emptyList()Nunca retorne null
DTO / RecordsList.copyOf()Cópia defensiva, garante imutabilidade
Testes unitáriosList.of()Conciso e claro
Frase para entrevista
"List.of() creates an immutable list — it's compact, null-safe, and thread-safe. new ArrayList<>() creates a mutable list that I can modify. I use List.of() for constants and return values, ArrayList for building collections."

3. ArrayList vs LinkedList​

ArrayList (vetor dinâmico) :
┌───┬───┬───┬───┬───┬───┬───┬ ┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │...│ ← acesso direto por índice
└───┴───┴───┴───┴───┴───┴───┴ ┘

LinkedList (lista duplamente encadeada) :
null ← [A] ⇄ [B] ⇄ [C] ⇄ [D] → null
OperaçãoArrayListLinkedList
get(index)O(1)O(n)
add(element) (final)O(1) amortizadoO(1)
add(index, element) (meio)O(n)O(1) se já tem o nó
remove(index)O(n)O(1) se já tem o nó
MemóriaCompactaMais pesada (ponteiros)
Cache CPUBoa (elementos contíguos)Ruim (elementos dispersos)
Na prática

Use ArrayList em 99% dos casos. LinkedList raramente é mais rápida na prática devido à localidade do cache da CPU. Mesmo para inserções frequentes, ArrayList costuma ser mais rápida.

Frase para entrevista

"I almost always use ArrayList. LinkedList has better theoretical complexity for insertions, but ArrayList wins in practice due to CPU cache locality. LinkedList is useful as a Queue (FIFO) via the Deque interface."


4. HashMap — Como funciona?​

Estrutura interna​

HashMap (Java 8+) :
┌──────────────────────────────┐
│ Buckets (vetor) │
├──────┬───────────────────────┤
│ [0] │ null │
│ [1] │ → Entry(key1, val1) │
│ [2] │ null │
│ [3] │ → Entry(key2, val2) → Entry(key3, val3) ← colisão!
│ [4] │ null │
│ ... │ │
│ [n] │ → Entry(keyN, valN) │
└──────┴───────────────────────┘

Se uma lista encadeada em um bucket ultrapassa 8 elementos → convertida em árvore rubro-negra (O(log n))

Como funciona?​

Map<String, Integer> map = new HashMap<>();
map.put("Alice", 25);

// Etapa 1: hashCode("Alice") → 63650026
// Etapa 2: 63650026 % tamanho_vetor → índice do bucket
// Etapa 3: Armazena Entry("Alice", 25) nesse bucket

Colisão = mesmo bucket​

// Se duas chaves têm o mesmo índice de bucket:
map.put("Alice", 25); // bucket 3
map.put("Bob", 30); // bucket 3 também (colisão)

// Java cria uma lista encadeada no bucket 3:
// bucket[3] → Entry("Alice", 25) → Entry("Bob", 30)
// Para encontrar "Bob", o Java percorre a lista e usa equals()

Complexidades​

OperaçãoCaso médioPior caso (muitas colisões)
put()O(1)O(log n) (árvore desde Java 8)
get()O(1)O(log n)
remove()O(1)O(log n)
containsKey()O(1)O(log n)
containsValue()O(n)O(n)

5. HashMap vs TreeMap vs LinkedHashMap​

MapOrdemComplexidadeQuando usar
HashMapNenhuma ordem garantidaO(1)Padrão, mais rápido
TreeMapOrdenado por chave (natural ou Comparator)O(log n)Precisa de chaves ordenadas
LinkedHashMapOrdem de inserçãoO(1)Precisa da ordem de inserção
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("banana", 2);
hashMap.put("apple", 1);
hashMap.put("cherry", 3);
// Iteração: ordem aleatória

Map<String, Integer> treeMap = new TreeMap<>();
treeMap.put("banana", 2);
treeMap.put("apple", 1);
treeMap.put("cherry", 3);
// Iteração: apple → banana → cherry (alfabética)

Map<String, Integer> linkedMap = new LinkedHashMap<>();
linkedMap.put("banana", 2);
linkedMap.put("apple", 1);
linkedMap.put("cherry", 3);
// Iteração: banana → apple → cherry (ordem de inserção)

6. HashSet vs TreeSet vs LinkedHashSet​

SetOrdemDuplicadosComplexidade
HashSetNenhumaNãoO(1)
TreeSetOrdenadoNãoO(log n)
LinkedHashSetOrdem de inserçãoNãoO(1)
// HashSet = o mais usado
Set<String> unique = new HashSet<>(List.of("b", "a", "c", "a"));
// → [a, b, c] (sem duplicados, ordem aleatória)

// TreeSet = ordenado automaticamente
Set<String> sorted = new TreeSet<>(List.of("banana", "apple", "cherry"));
// → [apple, banana, cherry]
Lembrete

HashSet usa hashCode() e equals() para detectar duplicados. Se seus objetos customizados não implementarem esses métodos, o Set não funcionará corretamente.


7. Queue e Deque​

// Queue (FIFO: First In, First Out) — fila
Queue<String> queue = new LinkedList<>();
queue.offer("primeiro"); // adiciona ao final
queue.offer("segundo");
queue.offer("terceiro");
queue.poll(); // remove do início → "primeiro"

// Deque (Double-Ended Queue) — pilha ou fila
Deque<String> deque = new ArrayDeque<>();

// Como Stack (LIFO):
deque.push("a");
deque.push("b");
deque.pop(); // → "b" (último a entrar, primeiro a sair)

// Como Queue (FIFO):
deque.offerLast("a");
deque.offerLast("b");
deque.pollFirst(); // → "a" (primeiro a entrar, primeiro a sair)
Não use Stack

java.util.Stack é uma classe legacy (Java 1.0) sincronizada desnecessariamente. Use ArrayDeque no lugar.

// ❌ Legacy
Stack<String> stack = new Stack<>();

// ✅ Moderno
Deque<String> stack = new ArrayDeque<>();

8. Iterando sobre Coleções​

As 5 formas de iterar​

List<String> names = List.of("Alice", "Bob", "Charlie");

// 1. For-each (mais comum)
for (String name : names) {
System.out.println(name);
}

// 2. Stream API (funcional)
names.stream()
.filter(n -> n.startsWith("A"))
.forEach(System.out::println);

// 3. forEach com lambda
names.forEach(name -> System.out.println(name));

// 4. Iterator (quando precisa remover durante a iteração)
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.equals("Bob")) it.remove();
}

// 5. For clássico com índice (apenas ArrayList)
for (int i = 0; i < names.size(); i++) {
System.out.println(names.get(i));
}

Armadilha: ConcurrentModificationException​

// ❌ ERRO — modificar uma lista durante o for-each
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) list.remove(s); // 💥 ConcurrentModificationException
}

// ✅ Solução 1: Iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) it.remove(); // OK
}

// ✅ Solução 2: removeIf (Java 8+)
list.removeIf(s -> s.equals("b")); // OK, mais conciso

// ✅ Solução 3: Stream + filter (cria nova lista)
List<String> filtered = list.stream()
.filter(s -> !s.equals("b"))
.toList();
Frase para entrevista

"You can't modify a collection while iterating with for-each. I use Iterator.remove(), removeIf(), or streams to safely remove elements during iteration."


9. Coleções thread-safe​

CollectionThread-safe?Alternativa thread-safe
ArrayListNãoCopyOnWriteArrayList ou Collections.synchronizedList()
HashMapNãoConcurrentHashMap
HashSetNãoConcurrentHashMap.newKeySet()
LinkedListNãoConcurrentLinkedQueue
// ❌ HashMap NÃO é thread-safe
Map<String, Integer> map = new HashMap<>();

// ✅ ConcurrentHashMap é thread-safe SEM sincronização global
Map<String, Integer> map = new ConcurrentHashMap<>();

// ❌ Hashtable é thread-safe mas LEGACY e lento (sincronizado em tudo)
Map<String, Integer> map = new Hashtable<>(); // NÃO USE
Frase para entrevista

"For concurrent access, I use ConcurrentHashMap instead of HashMap — it uses lock striping for better throughput than the legacy synchronized Hashtable."


10. Stream API — Resumo rápido​

List<User> users = getUsers();

// Filtrar
List<User> active = users.stream()
.filter(User::isActive)
.toList();

// Transformar
List<String> emails = users.stream()
.map(User::getEmail)
.toList();

// Ordenar
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::getName))
.toList();

// Agrupar
Map<String, List<User>> byRole = users.stream()
.collect(Collectors.groupingBy(User::getRole));

// Reduzir (soma, max, min)
double totalSalary = users.stream()
.mapToDouble(User::getSalary)
.sum();

// Verificar
boolean anyAdmin = users.stream().anyMatch(u -> "ADMIN".equals(u.getRole()));
boolean allActive = users.stream().allMatch(User::isActive);
boolean noneBlocked = users.stream().noneMatch(User::isBlocked);

As operações​

TipoExemplosRetorna
Intermediárias (lazy)filter, map, sorted, distinct, limit, skipStream
Terminais (executam)toList, collect, forEach, count, sum, reduce, anyMatchResultado
Frase para entrevista

"Streams are lazy — intermediate operations like filter and map don't execute until a terminal operation like toList() is called. I use streams for declarative, readable data processing."


Tabela resumo — Qual coleção usar?​

NecessidadeCollectionPor quê
Lista ordenada, acesso por índiceArrayListO(1) para get/set
Sem duplicadosHashSetO(1) para add/contains
Sem duplicados + ordenadoTreeSetO(log n), ordenado
Chave → ValorHashMapO(1) para put/get
Chave → Valor, ordenado por chaveTreeMapO(log n), ordenado
FIFO (fila)ArrayDequeMais rápido que LinkedList
LIFO (pilha)ArrayDequeSubstitui Stack (legacy)
Map thread-safeConcurrentHashMapLock striping
Lista imutávelList.of()Thread-safe, compacta

⬅️ Capítulo 6: Armadilhas Java | ⬅️ Voltar ao Módulo 18