Introdução
A otimização de código é um aspecto fundamental no desenvolvimento de software. A soma de múltiplas melhorias sutis pode resultar em ganhos significativos de desempenho e eficiência. Os principais objetivos incluem reduzir o volume do código e aumentar a velocidade de execução.
1. Uso do Modificador final
Aplicar o modificador final a classes e métodos permite ao compilador Java realizar otimizações como inlining. Classes declaradas como final não podem ser herdadas, e métodos final não podem ser sobrescritos. Se uma classe é final, todos os seus métodos são implicitamente final.
2. Reutilização de Objetos
Prefira StringBuilder ou StringBuffer para operações de concatenação de strings, evitando a criação excessiva de objetos temporários. O custo de alocação e coleta de lixo impacta diretamente o desempenho.
3. Preferência por Variáveis Locais
Variáveis locais, incluindo parâmetros, residem na pilha de execução (stack) e são destruídas ao final do escopo, sem necessidade de coleta de lixo. Variáveis estáticas e de instância ocupam o heap, com ciclo de vida mais longo.
4. Gerenciamento de Recursos
Recursos como conexões de banco de dados e streams de I/O devem ser liberados prontamente. O uso do try-with-resources (Java 7+) garante o fechamento automático.
5. Minimizar Cálculos em Laços
Armazene o valor de métodos chamados frequentemente em laços em uma variável local, evitando reavaliação.
// Ineficiente
for (int i = 0; i < collection.size(); i++) { ... }
// Otimizado
for (int i = 0, len = collection.size(); i < len; i++) { ... }
6. Inicialização Preguiçosa
Crie objetos apenas quando estritamente necessário, preferencialmente no ponto de uso.
// Criação imediata
String valor = "padrao";
if (condicao) {
lista.add(valor);
}
// Inicialização preguiçosa
if (condicao) {
String valor = "padrao";
lista.add(valor);
}
7. Uso Criterioso de Exceções
Exceções são custosas devido à captura do stack trace. Utilize-as para condições de erro, não para controle de fluxo. Evite blocos try-catch dentro de laços.
8. Capacidade Inicial para Coleções Baseadas em Array
Coleções como ArrayList, HashMap e StringBuilder podem ter sua capacidade inicial definida. Redimensionamentos frequentes são custosos. Para HashMap, uma boa prática é definir a capacidade inicial como uma potência de 2, considerando a taxa de carga.
9. Cópia de Arays com System.arraycopy()
Este método nativo é a maneira mais eficiente de copiar dados entre arrays.
10. Otimização Aritmética com Deslocamento de Bits
Operações de multiplicação e divisão por potências de 2 são mais rápidas quando expressas como deslocamento de bits.
// Operação convencional
int resultado = valor * 8;
// Otimizado com deslocamento
int resultado = valor << 3; // Deslocamento de 3 bits para a esquerda equivale a multiplicar por 8
11. Reutilização de Referências em Laços
Evite alocar um novo objeto a cada iteração. Declare a referência fora do laço e reutilize-a.
// Ineficiente: cria N objetos e referências
for (int i = 0; i < n; i++) {
MeuObjeto obj = new MeuObjeto();
// ...
}
// Otimizado: reutiliza a referência
MeuObjeto obj;
for (int i = 0; i < n; i++) {
obj = new MeuObjeto();
// ...
}
12. Seleção entre Array e ArrayList
Utilize arrays primitivos quando o tamanho for fixo e conhecido. ArrayList oferece flexibilidade para coleções dinâmicas.
13. Preferir Coleções Não Sincronizadas
Para código single-thread, ArrayList, HashMap e StringBuilder são mais eficientes que suas contrapartes sincronizadas (Vector, Hashtable, StringBuffer).
14. Padrão Singleton
Utilize o padrão Singleton para controlar a criação de instâncias de objetos custosos, promovendo reutilização e gerenciamento centralizado.
15. Uso Moderado de Variáveis Estáticas
Variáveis estáticas mantêm referências a objetos durante todo o ciclo de vida da classe, impedindo a coleta de lixo. Use-as com parcimônia, especialmente para objetos grandes.
16. Iteração Otimizada em Coleções
Para Lists que implementam RandomAccess (como ArrayList), iterações com loop for por índice são mais rápidas. Para outras listas (LinkedList), o uso de Iterator (ou for-each) é mais eficiente.
List<String> lista = ...;
if (lista instanceof RandomAccess) {
for (int i = 0; i < lista.size(); i++) {
String item = lista.get(i);
// ...
}
} else {
for (String item : lista) {
// ...
}
}
17. Declaração de Constantes
Declare constantes como static final. O compilador pode integrar seus valores diretamente no código (inlining), evitando acesso à variável em tempo de execução.
18. Uso de Pools
Utilize pools de conexões de banco de dados e pools de threads para reutilizar objetos caros, reduzindo a sobrecarga de criação e destruição.
19. Streams com Buffer
Para operações de I/O, utilize BufferedReader, BufferedWriter, BufferedInputStream e BufferedOutputStream para minimizar o número de acessos diretos ao sistema de arquivos ou rede.
20. Escolha da Coleção Adequada
Para acesso aleatório frequente e inserções no final, utilize ArrayList. Para inserções e remoções freqeuntes no meio da coleção, LinkedList pode ser mais eficiente.
21. Número de Parâmetros em Métodos Públicos
Reduza o número de parâmetros em métodos públicos. Parâmetros demais dificultam a manutenção e aumentam a chance de erros. Considere encapsular conjuntos de parâmetros em objetos.
22. Comparação de Strings
Para evitar NullPointerException, coloque a constante String ou o objeto que sabe-se não ser nulo no lado esquerdo da comparação equals().
// Pode lançar NPE se 'nomeUsuario' for null
if (nomeUsuario.equals("admin")) { ... }
// Seguro contra NPE
if ("admin".equals(nomeUsuario)) { ... }
23. Evitar Castings Perigosos
Realizar downcasting de tipos primitivos (long para int) pode levar a perda de dados inesperada e difícil de depurar. Prefira conversões explícitas e seguras, ou utilize métodos de parsing.
24. Limpeza de Coleções Compartilhadas
Em coleções compartilhadas (ex.: caches, singletons), remova prontamente os elementos que não são mais necessários para evitar vazamentos de memória.
25. Conversão de Primitivos para String
A ordem de eficiência para converter um tipo primitivo (como int) para String é:
Integer.toString(valor)String.valueOf(valor)(internamente chamatoString()após uma verificação de nulo)valor + ""(utilizaStringBuilder, mais lento)
26. Iteração em Map
Para iterar sobre chaves e valores de um Map, o método mais eficiente é iterar sobre o entrySet().
Map<String, Integer> mapa = ...;
for (Map.Entry<String, Integer> entrada : mapa.entrySet()) {
String chave = entrada.getKey();
Integer valor = entrada.getValue();
// ...
}
27. Fechamento de Múltiplos Recursos
Ao lidar com múltiplos recursos que precisam ser fechados (e.g., streams), garanta que o fechamento de cada recurso seja tratado individualmente para evitar que uma exceção no fechamento de um impeça o fechamento dos outros. Utilize try-with-resources aninhados ou blocos separados.
// Abordagem segura para múltiplos recursos
try (RecursoA resA = new RecursoA()) {
try (RecursoB resB = new RecursoB()) {
// uso dos recursos
}
} // resB é fechado aqui, depois resA.