Capítulo 7: Coleções Java — O guia para entrevistas
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ção | Tipo | Modificável? | Aceita null? |
|---|---|---|---|
List.of("a", "b") | Lista imutável | Não | Não |
new ArrayList<>(List.of("a", "b")) | Lista mutável | Sim | Sim |
Collections.unmodifiableList(list) | View imutável | Não (mas a original sim) | Sim |
List.copyOf(list) | Cópia imutável | Não | Nã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ção | Usar | Por quê |
|---|---|---|
| Constantes, configurações | List.of() | Imutável, thread-safe |
| Construir uma lista progressivamente | new ArrayList<>() | Precisa adicionar elementos |
| Retorno de método (caso vazio) | List.of() ou Collections.emptyList() | Nunca retorne null |
| DTO / Records | List.copyOf() | Cópia defensiva, garante imutabilidade |
| Testes unitários | List.of() | Conciso e claro |
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ção | ArrayList | LinkedList |
|---|---|---|
get(index) | O(1) | O(n) |
add(element) (final) | O(1) amortizado | O(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ória | Compacta | Mais pesada (ponteiros) |
| Cache CPU | Boa (elementos contíguos) | Ruim (elementos dispersos) |
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.
"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ção | Caso médio | Pior 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
| Map | Ordem | Complexidade | Quando usar |
|---|---|---|---|
| HashMap | Nenhuma ordem garantida | O(1) | Padrão, mais rápido |
| TreeMap | Ordenado por chave (natural ou Comparator) | O(log n) | Precisa de chaves ordenadas |
| LinkedHashMap | Ordem de inserção | O(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
| Set | Ordem | Duplicados | Complexidade |
|---|---|---|---|
| HashSet | Nenhuma | Não | O(1) |
| TreeSet | Ordenado | Não | O(log n) |
| LinkedHashSet | Ordem de inserção | Não | O(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]
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)
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();
"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
| Collection | Thread-safe? | Alternativa thread-safe |
|---|---|---|
ArrayList | Não | CopyOnWriteArrayList ou Collections.synchronizedList() |
HashMap | Não | ConcurrentHashMap |
HashSet | Não | ConcurrentHashMap.newKeySet() |
LinkedList | Não | ConcurrentLinkedQueue |
// ❌ 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
"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
| Tipo | Exemplos | Retorna |
|---|---|---|
| Intermediárias (lazy) | filter, map, sorted, distinct, limit, skip | Stream |
| Terminais (executam) | toList, collect, forEach, count, sum, reduce, anyMatch | Resultado |
"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?
| Necessidade | Collection | Por quê |
|---|---|---|
| Lista ordenada, acesso por índice | ArrayList | O(1) para get/set |
| Sem duplicados | HashSet | O(1) para add/contains |
| Sem duplicados + ordenado | TreeSet | O(log n), ordenado |
| Chave → Valor | HashMap | O(1) para put/get |
| Chave → Valor, ordenado por chave | TreeMap | O(log n), ordenado |
| FIFO (fila) | ArrayDeque | Mais rápido que LinkedList |
| LIFO (pilha) | ArrayDeque | Substitui Stack (legacy) |
| Map thread-safe | ConcurrentHashMap | Lock striping |
| Lista imutável | List.of() | Thread-safe, compacta |