Escolhendo as estruturas certas antes de começar a codar
Eu passei horas debuggando um sistema em produção porque alguém usava ArrayList quando deveria ter usado HashMap. A consulta que levava 3 segundos virou 45 minutos depois que o volume de dados cresceu. Isso não é um problema teórico. Estrutura de dados java é algo que você sente na prática quando a aplicação começa a travar. Vou explicar como funciona na prática, com exemplos reais e armadilhas que ninguém conta no tutorial básico.
estrutura de dados java: o que realmente importa no dia a dia
A maioria dos desenvolvedores Java conhece as definições de livro. Saber que ArrayList é uma lista dinâmica e HashMap é uma tabela de dispersão não vai te salvar quando o sistema for para produção. O que importa é entender quando cada uma se comporta mal e como evitar isso. Vamos começar com algo concreto. Você precisa armazenar IDs de usuários e buscar por eles constantemente. A tentação é usar ArrayList e fazer um loop para encontrar o item certo. Isso funciona quando você tem 50 registros. Quando chega a 50 mil, seu sistema fica lento e você não entende o porquê.
Um HashMap resolve isso porque a busca é O(1) em vez de O(n). A diferença entre 3 segundos e 45 minutos existe exatamente aí. Não é mágica, é complexidade algorítmica.
Listas e arrays dinâmicos
ArrayList é a estrutura mais usada no Java e também a mais mal empregada. Ela armazena elementos em um array interno que cresce automaticamente quando atinge a capacidade. O custo de crescimento é amortecido, mas não é gratuito. Quando o ArrayList precisa redimensionar, ele cria um novo array maior e copia todos os elementos. Isso consome memória e processamento no momento exato em que você menos espera. Se você sabe aproximadamente quantos elementos vai ter, sempre inicialize com a capacidade esperada:
List<String> lista = new ArrayList<>(1000); Isso evita resizes desnecessários e melhora a performance desde o início. Eu vi isso economizar cerca de 200ms em um processamento de lote com 10 mil itens. Pode parecer pouco, mas em sistemas que rodam centenas de vezes por dia, isso se soma rapidamente.
A desvantagem do ArrayList é a inserção e remoção no meio da lista. Cada operação assim força um deslocamento de todos os elementos subsequentes. Se você precisa fazer isso frequentemente, considere LinkedList. A diferença de performance entre ArrayList e LinkedList para inserções no meio é enorme: LinkedList é O(1) para inserção após ter o nó, mas ArrayList é O(n) por causa do deslocamento.
Maps e tabelas de dispersão
HashMap é provavelmente a estrutura mais poderosa do Java e também a mais incompreendida. Ela mapeia chaves para valores usando uma função de hash. A teoria diz que a busca deve ser O(1), mas a prática tem nuances importantes. O primeiro detalhe é que HashMap permite uma chave null e um valor null. Isso parece inocente, mas causa confusão quando você espera um NullPointerException e ele não acontece. Se você precisa de restrições mais rígidas, TreeMap pode ser uma alternativa, embora seja mais lento para buscas.
Outro problema real que eu enfrentei: usar objetos mutáveis como chave em HashMap. Eu tinha um problema onde uma classe Persona era usada como chave, e alguém modificava o nome da pessoa depois de inserir no mapa. O HashMap não encontrava mais o elemento porque o hashCode mudou. Isso gera comportamentos estranhos que parecem bugs aleatórios. Para evitar isso, use apenas objetos imutáveis como chaves ou siga a regra: nunca modifique um objeto que é chave em um HashMap depois de inserido. Se precisar de chaves compostas, crie uma classe imutável com equals e hashCode bem implementados, ou use uma estrutura como AbstractMap.SimpleEntry.
HashMap também tem um fator de carga padrão de 0.75. Quando o número de entradas ultrapassa 75% da capacidade, o mapa é redimensionado. Se você tem um cenário conhecido de muitos elementos, inicialize com capacidade e fator de carga ajustados para evitar resizes: Map<String, Integer> map = new HashMap<>(2000, 0.75f);
Conjuntos e coleções únicas
HashSet usa HashMap internamente. Ele armazena apenas os elementos como chaves, com um objeto constante como valor. Isso significa que todas as regras de HashMap se aplicam a HashSet também. HashSet é útil quando você precisa verificar rapidamente se um elemento existe ou remover duplicados. Para remover duplicados de uma lista:
👉 Clique no botão abaixo para saber mais sobre o assunto!
List<String> listaComDuplicatas = Arrays.asList("a", "b", "a", "c", "b"); Isso preserva a ordem de inserção se você usar LinkedHashSet no lugar de HashSet. A diferença é sutil mas importante: HashSet não garante ordem, LinkedHashSet preserva a ordem em que os elementos foram adicionados.
List<String> unica = new ArrayList<>(new HashSet<>(listaComDuplicatas));
Filas e pilhas
Queue e Stack são interfaces. A implementação mais comum de Queue é LinkedList, mas para filas com alta concorrência ou necessidade de performance, PriorityQueue pode ser mais adequado dependendo do caso. Stack é uma classe antiga do Java. A recomendação oficial é usar Deque com ArrayDeque:
Deque<String> pilha = new ArrayDeque<>(); ArrayDeque é mais rápido que Stack porque não tem sobrecarga de sincronização desnecessária. Stack é sincronizado por padrão, o que adiciona overhead em cenários single-thread.
pilha.push("primeiro");
pilha.push("segundo");
String topO = pilha.pop();
Performance e cenários reais
Vamos a números concretos. Em uma aplicação que processava pedidos, eu tinha um código que fazia busca linear em uma lista de 15 mil itens a cada interação do usuário. Migrei para HashMap e o tempo de resposta caiu de 800ms para 2ms. A mudança foi praticamente instantânea do ponto de vista do usuário. Mas HashMap não é solução para tudo. Se você precisa percorrer todos os elementos em ordem, HashMap não garante ordem. Use LinkedHashMap se precisar de ordem de inserção, ou TreeMap se precisar de ordem natural dos elementos.
Outro caso prático: eu tive um problema com ConcurrentHashMap em um cenário de alta concorrência. O código usava HashMap normal em múltiplas threads e os resultados eram inconsistentes. A troca para ConcurrentHashMap resolveu, mas introduziu um leve overhead de sincronização. Em benchmarks, ConcurrentHashMap é cerca de 10-15% mais lento que HashMap single-thread, mas é seguro para concorrência. A lição é: escolha a estrutura baseada no padrão de acesso, não na familiaridade. Se você só insere e lê raramente, ArrayList pode ser suficiente. Se há muitas buscas, HashMap é essencial. Se há concorrência, considere ConcurrentHashMap ou CopyOnWriteArrayList dependendo do padrão de escrita.
Recursos da biblioteca padrão
Todas essas estruturas já vêm com o Java. Não precisa baixar nada. As principais classes estão no pacote java.util e estão disponíveis em qualquer projeto Java padrão. Para projetos Maven, nenhuma dependência extra é necessária. Se você precisa de estruturas mais avançadas como B-trees ou grafos, bibliotecas como Google Guava ou Eclipse Collections oferecem alternativas, mas para a grande maioria dos casos, a coleção padrão do Java é suficiente.
Erros comuns que atrasam desenvolvimento
O erro mais frequente que eu vejo é usar == ao invés de .equals() para comparar strings em coleções. HashMap usa hashCode e equals para determinar igualdade de chaves. String implementa ambos corretamente, então instanceof funciona. Mas se você criar uma classe própria e esquecer de sobrescrever equals e hashCode, o HashMap vai tratar objetos iguais como diferentes. Isso gera perdas de dados silenciosas. Um elemento é inserido duas vezes com conteúdo idêntico, e você não percebe até tentar recuperar e não encontrar.
Outro erro é esquecer que iteradores são fail-fast. Se você modificar uma coleção enquanto itera sobre ela, recebe ConcurrentModificationException. A solução é usar iterator.remove() ou coletar os itens para remoção e processar depois do loop.
Quando cada estrutura falha
ArrayList falha quando há muitas inserções e remoções no meio da lista. O custo de deslocamento de elementos é linear e cresce com o tamanho da lista. Se o seu cenário envolve esse tipo de operação frequentes, considere LinkedList ou até mesmo uma estrutura baseada em árvore. HashMap falha quando há muitas colisões de hash. Isso é raro com String e Integer como chaves, mas pode acontecer com classes mal implementadas. Se você notar que o HashMap está se comportando pior que O(1), verifique se equals e hashCode estão consistentes na sua classe de chave.
ConcurrentHashMap falha quando a carga de leitura é muito menor que a de escrita. Neste caso, o overhead de sincronização pode ser maior que o benefício da segurança. Avalie se um HashMap simples com controle externo de lock é mais adequado. A escolha certa de estrutura de dados java depende de entender o padrão de acesso do seu sistema. Teste com dados reais antes de decidir. Benchmarks teóricos são úteis, mas dados do seu próprio sistema são mais confiáveis que qualquer artigo na internet.