Nos aplicativos criados no dia-a-dia pelos desenvolvedores sempre temos que lidar com a criação de objetos. Apesar do assunto parecer simples e inofensivo, existem diversas armadilhas que podem aparecer e levar a sérios problemas de performance que podem comprometer a aplicação.
Uma situação frequente e apropriada é a reutilização de um objeto individual ao invés de criarmos um novo objeto funcionalmente equivalente sempre que este se torna necessário. Além da reutilização ser mais elegante, ela é extremamente mais rápida.
Para começar com um exemplo que apesar de ser simples é bastante claro, é interessante primeiramente olharmos com bastante atenção o código abaixo:
Listagem 1: Criando uma String utilizando seu construtor
String s = new String("Qualquer coisa");
Essa simples instrução cria uma nova instância de String sempre que ela é executada, mas nenhuma dessas criações de objetos é realmente necessária. O valor da instância dessa String é o argumento passado pelo construtor.
É interessante notar que se esse tipo de utilização ocorrer dentro de um loop, teremos centenas ou milhões de instâncias de Strings sendo criadas, o que pode comprometer consideravelmente a performance de uma aplicação.
A melhor forma de criar uma String nestas condições seria utilizar uma simples inicialização por atribuição direta:
Listagem 2: Criando uma String de forma explicita
String s = "Qualquer coisa";
Essa versão usa apenas uma instância de String sempre que ela for executada, ao invés de criar uma nova instância. Outra situação importante é que esse mesmo objeto será reutilizado por qualquer outro código que estiver sendo executado na mesma máquina virtual (Java Virtual Machine - JVM) que tiver esse mesmo literal String "Qualquer coisa".
Nas próximas seções veremos outras formas de evitarmos a criação desnecessária de objetos em Java através dos métodos de fabricação estáticos ou através da utilização de tipos primitivos.
Evitando a criação de instâncias desnecessárias com métodos de fabricação estáticos
Uma dica geral para evitar a criação de instâncias de objetos desnecessárias é usarmos os métodos de fabricação estáticos ao invés de utilizar os construtores das classes. Por exemplo, o método de fabricação para um Boolean pode ser utilizado através do seu método de fabricação estático chamado valueOf(). Para utilizarmos o seu método de fabricação estático para uma dada String poderíamos utilizar o código abaixo:
Listagem 3: Criando um Boolean passando uma String para seu método de fabricação estático
Boolean.valueOf(String)
Essa é a forma preferível, se comparada com utilizar o código abaixo:
Listagem 4: Criando um Boolean passando uma String para seu construtor
Boolean(String)
O código acima terá os mesmos problemas já relatados anteriormente, como no caso da utilização da classe String. Ou seja, o construtor sempre criará um novo objeto quando for executado, ao contrário do método de fabricação estático que nunca será solicitado a fazer isso.
Evitando a criação de instâncias desnecessárias no Java 5
O Java 5 também pode complicar o desenvolvedor na criação de instâncias desnecessárias, o que pode acontecer quando utilizamos o encaixotamento automático ou autoboxing. Para cada tipo primitivo existe um tipo encaixotado como boolean (primitivo) e Boolean (encaixotado), int (primitivo) e Integer (encaixotado), long (primitivo) e Long (encaixotado) e assim por diante. Apesar das diferenças semânticas serem sutis, as diferenças de desempenho estão bem longe de serem. Como um exemplo vamos analisar o código abaixo que calcula uma simples soma.
Listagem 4: Criando uma soma de inteiros positivos
public static void main(String [] args) {
Long soma = 0L;
for (long i=0; i < Integer.MAX_VALUE; i++) {
soma += i;
}
System.out.println(soma);
}
Nesta aplicação de soma demonstrada acima, tivemos que declarar uma variável “soma” como um tipo Long, pois um inteiro não seria capaz de armazenar toda essa soma. Apesar de parecer simples e inofensivo, esse programa é terrivelmente lento. Apesar da resposta vir correta, o nível de performance é muito baixo. A variável soma está declarada como um Long ao invés de um long de tipo primitivo e isso foi o que influenciou a sua péssima performance, pois temos aproximadamente 2 na potência de 31 objetos desnecessários sendo criados neste simples programa. Se mudarmos o Long externo para long, teremos um tempo de execução de 43 segundos para 6,8 segundos, um ganho incrível com uma simples alteração.
Portanto, outra regra clara aqui é sempre preferirmos primitivos simples do que primitivos encaixotados.
Conclusão
Neste artigo tivemos a oportunidade de investigarmos alguns casos críticos de problemas de performance que normalmente acontecem em diversas aplicações desenvolvidas em Java. Esses problemas frequentemente ocorrem por causa da criação de objetos desnecessários ou por causa da má utilização dos tipos encaixotados (ou autoboxing, como é mais conhecido). Verificamos diferentes formas de criar objetos e evitar problemas corriqueiros que podem comprometer a performance das aplicações em Java. O ganho de performance utilizando essas medidas simples e eficazes pode melhorar em mais de 500% a performance de uma aplicação.
Bibliografia
- What is Autoboxing and Unboxing in Java – Example Tutorial and Corner cases;
- Autoboxing and Unboxing;
- Java Efetivo 2º edição. Joshua Bloch. Alta Books, 2010.
- Strings Java;