Técnicas Práticas para Otimização de Desempenho em Java

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 é:

  1. Integer.toString(valor)
  2. String.valueOf(valor) (internamente chama toString() após uma verificação de nulo)
  3. valor + "" (utiliza StringBuilder, 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.

Tags: java desempenho Otimização de Código JVM Coleções

Publicado em 6-6 19:58 por Thomas