VapVup👤
← Voltar
📦

Collections Framework

O Collections Framework é a espinha dorsal da programação Java. Dominar suas interfaces, implementações e trade-offs de performance é o que separa um desenvolvedor que 'usa Java' de um que 'pensa em Java'. Vamos mergulhar nos detalhes que frameworks escondem de você.

🏙️Analogia

A Cidade das Coleções

Imagine o Collections Framework como uma cidade. Collection é a prefeitura — define as regras gerais (add, remove, contains). List é a rua numerada — cada casa tem um endereço (índice). Set é o clube exclusivo — cada pessoa entra uma única vez. Map é a lista telefônica — você busca pelo nome (chave) e encontra o número (valor). Queue é a fila do banco — primeiro a chegar, primeiro a ser atendido. E Iterable? É o sistema de transporte público que permite percorrer toda a cidade.

💡Conceito

Hierarquia de Interfaces

Tudo começa com Iterable<E>, que define iterator(). Collection<E> extends Iterable e adiciona add(), remove(), contains(), size(). Daí se ramificam três sub-interfaces: List<E> (sequência ordenada com índices), Set<E> (sem duplicatas), e Queue<E> (FIFO). IMPORTANTE: Map<K,V> NÃO extends Collection — é uma hierarquia separada. Deque<E> extends Queue e adiciona operações nas duas pontas. SortedSet e NavigableSet estendem Set para conjuntos ordenados.

// Hierarquia simplificada:
// Iterable<E>
//   └── Collection<E>
//         ├── List<E>        → ArrayList, LinkedList, Vector
//         ├── Set<E>         → HashSet, LinkedHashSet
//         │     └── SortedSet<E>
//         │           └── NavigableSet<E> → TreeSet
//         └── Queue<E>       → PriorityQueue
//               └── Deque<E> → ArrayDeque, LinkedList
//
// Map<K,V> (hierarquia separada)
//   ├── HashMap, LinkedHashMap
//   └── SortedMap<K,V>
//         └── NavigableMap<K,V> → TreeMap
📚Analogia

ArrayList vs LinkedList — Estante vs Corrente

ArrayList é uma estante com prateleiras numeradas — você olha direto para a prateleira 42 e pega o livro (O(1)). Mas inserir um livro no meio? Precisa empurrar todos os livros para o lado. LinkedList é uma corrente — cada elo aponta para o próximo e o anterior. Inserir no meio é fácil (abre um elo e encaixa), mas encontrar o elo 42? Conta um por um desde o início. Na prática, a estante (ArrayList) vence quase sempre porque CPUs modernas adoram dados contíguos na memória (cache lines).

🔧Exemplo

ArrayList — Por Dentro do Array Dinâmico

ArrayList usa um Object[] interno que cresce automaticamente. A capacidade inicial padrão é 10. Quando o array enche, um novo array com ~1.5x do tamanho é alocado (oldCapacity + oldCapacity >> 1) e os elementos são copiados. Isso significa que add() no final é O(1) amortizado — a maioria das vezes é instantâneo, mas ocasionalmente há uma cópia O(n). Se você sabe o tamanho aproximado, use new ArrayList<>(expectedSize) para evitar redimensionamentos desnecessários.

// Criação com capacidade inicial
List<String> list = new ArrayList<>(1000); // evita ~7 redimensionamentos

// Diferenças sutis entre fábricas de listas
List<String> fixed = Arrays.asList("a", "b", "c"); // tamanho fixo, set() OK
List<String> immutable = List.of("a", "b", "c");    // imutável total
List<String> mutable = new ArrayList<>(List.of("a", "b", "c")); // totalmente mutável

// fixed.add("d");      → UnsupportedOperationException (tamanho fixo)
// immutable.set(0, "x"); → UnsupportedOperationException (imutável)
mutable.add("d");         // OK!
mutable.set(0, "x");      // OK!
💡Conceito

O Contrato equals() / hashCode()

Este é o contrato mais importante do Collections Framework. Se a.equals(b), então a.hashCode() == b.hashCode(). A violação deste contrato quebra HashSet, HashMap e qualquer coleção baseada em hash. O inverso NÃO é verdade: dois objetos com mesmo hashCode não precisam ser equals (colisão de hash é normal). SEMPRE sobrescreva hashCode() quando sobrescrever equals(). Use Objects.hash() para gerar o hashCode com os mesmos campos do equals(). A partir do Java 16, Records geram ambos automaticamente.

📬Analogia

HashMap — O Armário de Correio

HashMap é um armário de correio com caixas numeradas (buckets). Quando você guarda uma carta, o hashCode() determina o número da caixa (hash & (capacity-1)). Se duas cartas caem na mesma caixa (colisão), elas ficam empilhadas lá dentro como uma listinha. Em Java 8+, se a pilha ficar muito alta (>8 cartas), o carteiro reorganiza em uma árvore para encontrar mais rápido. O load factor (0.75) é como a regra: 'quando 75% das caixas estiverem ocupadas, compre um armário maior e redistribua todas as cartas'.

🔧Exemplo

HashMap Internals — Capacidade, Load Factor e Treeificação

HashMap armazena dados em um Node<K,V>[] de tamanho sempre potência de 2. O bucket é calculado como hash(key) & (capacity - 1) — por isso a capacidade deve ser potência de 2 (bit masking mais eficiente que módulo). Quando size > capacity × loadFactor, ocorre rehash: novo array com 2x capacidade e redistribuição de todas as entradas. Em Java 8+, buckets com mais de 8 entradas (TREEIFY_THRESHOLD) são convertidos de LinkedList para Red-Black Tree, melhorando o pior caso de O(n) para O(log n).

// Construtor otimizado quando se sabe o tamanho esperado
// Evita rehashes desnecessários
int expectedSize = 1000;
Map<String, User> users = new HashMap<>(
    (int) (expectedSize / 0.75f) + 1  // capacidade = 1334
);

// Java 8+ APIs modernas
users.computeIfAbsent("key", k -> new User(k));        // cria se não existir
users.computeIfPresent("key", (k, v) -> v.update());    // atualiza se existir
users.merge("key", newUser, (old, new_) -> old.merge(new_)); // merge inteligente
users.getOrDefault("missing", User.ANONYMOUS);           // valor padrão

// Iteração eficiente
users.forEach((key, value) -> process(key, value));
🏥Analogia

PriorityQueue — A Sala de Emergência

PriorityQueue funciona como a triagem de um hospital. Não importa quem chegou primeiro — quem tem prioridade mais alta (menor valor no min-heap) é atendido antes. Mas se você olhar a sala de espera (iterator), as pessoas NÃO estão sentadas em ordem de prioridade — elas estão espalhadas. Só quando o médico chama o próximo (poll()) é que o sistema garante pegar o mais urgente.

🔧Exemplo

ArrayDeque — O Canivete Suíço Moderno

ArrayDeque é a implementação mais eficiente para pilha (LIFO) e fila (FIFO) em Java. Usa um array circular — dois ponteiros (head e tail) que 'giram' ao redor do array. Não tem overhead de sincronização (como Stack/Vector) nem de alocação de nós (como LinkedList). O Javadoc oficial recomenda ArrayDeque sobre Stack e LinkedList para esses casos de uso.

// Como PILHA (LIFO) — substitui Stack
Deque<String> stack = new ArrayDeque<>();
stack.push("primeiro");   // insere no topo
stack.push("segundo");
String top = stack.pop();  // remove do topo → "segundo"
String peek = stack.peek(); // olha sem remover → "primeiro"

// Como FILA (FIFO) — substitui LinkedList como Queue
Deque<String> queue = new ArrayDeque<>();
queue.offer("primeiro");   // insere no final
queue.offer("segundo");
String head = queue.poll(); // remove do início → "primeiro"

// ArrayDeque NÃO aceita null (diferente de LinkedList)
// queue.offer(null); → NullPointerException
💡Conceito

Tabela de Complexidade Big-O

Esta é a tabela que todo desenvolvedor Java deveria ter memorizada. As complexidades determinam qual coleção usar em cada situação: • ArrayList: get(i) O(1) | add(end) O(1)* | add(i) O(n) | remove(i) O(n) | contains O(n) • LinkedList: get(i) O(n) | add(end) O(1) | add(i) O(1)† | remove(i) O(1)† | contains O(n) • HashSet: add O(1)* | contains O(1)* | remove O(1)* • TreeSet: add O(log n) | contains O(log n) | remove O(log n) | first/last O(log n) • HashMap: put O(1)* | get O(1)* | containsKey O(1)* | containsValue O(n) • TreeMap: put O(log n) | get O(log n) | containsKey O(log n) • PriorityQueue: offer O(log n) | poll O(log n) | peek O(1) | contains O(n) • ArrayDeque: push/pop O(1)* | offer/poll O(1)* | contains O(n) * = amortizado | † = se já tiver referência ao nó/iterator posicionado