Dicas de Performance em Java
Dicas práticas de performance em Java — meça antes, evite otimização prematura e micro-otimizações comuns.
Java é rápido o suficiente para quase tudo, mas código lento ainda acontece — geralmente por fazer trabalho demais, alocar memória em excesso ou escolher a estrutura de dados errada. A dica de performance mais importante não é um truque: meça antes de mudar qualquer coisa. Este capítulo explica como medir com honestidade, as otimizações que mais valem a pena (construção de strings, escolha de estruturas de dados, evitar alocações desnecessárias) e as armadilhas que tornam código "rápido" ingênuo na verdade lento.
Meça primeiro, otimize depois
Adivinhar sobre performance é como se gasta horas acelerando código que nunca foi lento. Perfilie uma carga de trabalho real, encontre o caminho crítico (a pequena fração do código onde a maior parte do tempo é gasta) e só então otimize. Para experimentos rápidos, System.nanoTime() fornece um delta de relógio; para benchmarks sérios, use uma ferramenta como o JMH (Java Microbenchmark Harness), que aquece o JIT e leva em conta o ruído de medição.
long start = System.nanoTime();
doWork();
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("Took " + elapsedMs + " ms");Duas regras acompanham isso. Primeiro, evite otimização prematura — código claro que é rápido o suficiente supera código inteligente que é difícil de ler. Segundo, o compilador JIT da JVM otimiza código quente em tempo de execução, então um método só atinge velocidade total após ser executado muitas vezes; uma única chamada cronometrada diz pouco.
Construa strings com StringBuilder
Como strings são imutáveis, s += x dentro de um loop cria uma string completamente nova e copia todos os caracteres anteriores a cada iteração — isso é trabalho O(n²). O StringBuilder mantém um buffer expansível e anexa no lugar, transformando o mesmo trabalho em O(n).
// Slow: a new String allocated every iteration
String csv = "";
for (String field : fields) {
csv += field + ",";
}
// Fast: one buffer, appended in place
StringBuilder sb = new StringBuilder();
for (String field : fields) {
sb.append(field).append(',');
}
String csv2 = sb.toString();Um único a + b + c fora de um loop é aceitável — o compilador já o transforma em um único StringBuilder (veja concatenação de strings para o que o compilador faz). O problema é a concatenação dentro de um loop, onde cada passagem adiciona mais uma cópia completa.
Escolha a estrutura de dados certa
Os maiores ganhos geralmente vêm de escolhas algorítmicas, não de micro-ajustes. Buscar um valor em um ArrayList percorre cada elemento (O(n)); um HashMap ou HashSet faz isso em tempo aproximadamente constante (O(1)). Escolha a coleção que corresponde à forma como você realmente acessa os dados.
| Necessidade | Use | Custo de busca |
|---|---|---|
| Acesso por índice, anexar no final | ArrayList | O(1) por índice, O(n) por valor |
| Busca por chave/valor | HashMap | O(1) médio |
| Teste de pertinência, sem duplicatas | HashSet | O(1) médio |
| Chaves ordenadas | TreeMap | O(log n) |
| Inserção/remoção frequente nas extremidades | ArrayDeque | O(1) nas extremidades |
Se você conhece o tamanho final, passe-o ao construtor: new ArrayList<>(10_000) ou new HashMap<>(capacity). Isso evita a realocação e cópia repetidas que ocorrem à medida que uma coleção cresce.
Evite criação desnecessária de objetos
Todo objeto alocado deve ser coletado depois, e a coleta de lixo não é gratuita. Reutilize valores imutáveis, prefira primitivos em vez de seus wrappers em loops intensivos e não crie objetos que você descarta imediatamente.
// Autoboxing: every += boxes a new Integer
Long total = 0L;
for (int i = 0; i < n; i++) total += i; // slow, allocates boxes
// Primitive: no allocation at all
long sum = 0L;
for (int i = 0; i < n; i++) sum += i; // fastOutros ganhos fáceis: armazene em cache objetos Pattern compilados em vez de chamar String.matches() em um loop, reutilize um DateTimeFormatter (é thread-safe e imutável) e prefira for aprimorado em vez de streams nos loops internos mais intensivos onde a alocação importa.
O que tirar da execução:
Same result? trueprova queStringBuilderproduz exatamente a mesma string que+=, então substituí-lo muda apenas a velocidade, nunca a correção.- A razão "x slower" impressa mostra que a concatenação em loop custa muito mais do que anexar a um buffer, porque cada
+=copia toda a string até então. Both lists size 100000: trueconfirma que umArrayListpré-dimensionado termina idêntico a um que cresceu — a dica do construtor afeta a alocação, não o conteúdo.Pre-sized faster? truemostra que informar aoArrayListsua capacidade antecipadamente evita as etapas repetidas de redimensionamento e cópia.Map lookups found: 50000 in ... msdemonstra que 50.000 buscas emHashMapterminam em aproximadamente um milissegundo, o retorno de escolher acesso O(1) em vez de uma varredura de lista O(n).
Armadilhas comuns
Alguns erros transformam código "obviamente mais rápido" no oposto:
- Confiar em uma única execução cronometrada. O JIT ainda não aqueceu, e o SO pode ter agendado outra coisa no meio da medição. Repita o trabalho milhares de vezes, ou use JMH, antes de acreditar em um número.
- Micro-otimizar código frio. Um método que executa uma vez na inicialização não ganha nada com um loop mais compacto. Invista esforço apenas no caminho crítico que o profiler aponta.
- Construção de strings com
+em um loop. A desaceleração evitável mais comum — useStringBuildersempre que concatenar dentro de um loop. - Autoboxing oculto. Um
List<Integer>,Map<Integer, Integer>ou um acumuladorLongperdido embala cada valor. Em um loop numérico intensivo, prefira primitivos e arrays de primitivos. - Otimizar antes de funcionar. Correto primeiro, rápido depois. Código claro que você pode perfilar supera código inteligente que você não consegue raciocinar.
Resumo
- Meça antes de mudar qualquer coisa — perfilie uma carga de trabalho real e otimize apenas o caminho crítico.
- Construa strings com
StringBuilder, não com+=, dentro de loops. - Combine a coleção com o padrão de acesso:
HashMap/HashSetpara buscas,ArrayListpara acesso indexado; pré-dimensione quando o tamanho for conhecido. - Evite alocação desnecessária: prefira primitivos, reutilize objetos imutáveis e lembre-se de que cada objeto adiciona trabalho de coleta de lixo.