Performance em Java para Classes do Dia a Dia

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (7)  (0)

Veja nesse artigo como otimizar a performance de aplicações Java ao explorar os mecanismos por trás da API básica.

Performance em Java

Figura 1: Performance em Java

Introdução

Muitas classes que usamos diariamente, como String, Boolean e Integer possuem “macetes” que às vezes passam despercebidos pelo desenvolvedor. Iremos abordar nesse artigo alguns desses aspectos, a fim de mostrar as alternativas mais performáticas no uso dessas classes.

1) Classe Boolean

Listagem 1: Criando Boolean via Construtor

public class CreateBoolean {
    
    public static final int COUNT = 1000;
    
    public static void main(String[] args) {
        
        Boolean b1 = null, b2 = null, b3 = null, b4 = null;
        
        final long startTime = System.nanoTime();
        
        for(int i = 0; i < COUNT; i++) {
            b1 = new Boolean("true");
            b2 = new Boolean("true");
            b3 = new Boolean("false");
            b4 = new Boolean("false");
        }
        
        final long estimatedTime = System.nanoTime() - startTime;
        
        System.out.println("Compare 1 = " + (b1 == b2));
        System.out.println("Compare 2 = " + (b3 == b4));
        
        System.out.println("Tempo decorrido = " + estimatedTime + " nanosegundos");
    }
}

Saída:

  • Compare 1 = false
  • Compare 2 = false
  • Tempo decorrido = 641950 nanosegundos

Cada chamada a new Boolean() cria um objeto novo na memória. A quantidade de objetos Boolean criados na aplicação será de 4*COUNT.

Analisando o código fonte da classe Boolean:

Listagem 2: Fragmento de código fonte de Boolean

public final class Boolean implements java.io.Serializable,
                                      Comparable<Boolean>
{
    /**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code true}.
     */
    public static final Boolean TRUE = new Boolean(true);

    /**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code false}.
     */
    public static final Boolean FALSE = new Boolean(false);

    /**
     * The Class object representing the primitive type boolean.
     *
     * @since   JDK1.1
     */
    public static final Class<Boolean> TYPE = Class.getPrimitiveClass("boolean");

    /**
     * Returns a {@code Boolean} with a value represented by the
     * specified string.  The {@code Boolean} returned represents a
     * true value if the string argument is not {@code null}
     * and is equal, ignoring case, to the string {@code "true"}.
     *
     * @param   s   a string.
     * @return  the {@code Boolean} value represented by the string.
     */
    public static Boolean valueOf(String s) {
        return toBoolean(s) ? TRUE : FALSE;
    }

    private static boolean toBoolean(String name) {
        return ((name != null) && name.equalsIgnoreCase("true"));
    }
    …

Vemos que existem duas constantes, TRUE e FALSE, declaradas na classe Boolean. Por ser uma classe imutável e thread-safe, podemos utilizar essas constantes para substituir a criação via new em qualquer lugar da aplicação, economizando memória e ganhando velocidade, pois menos objetos serão criados, diminuindo o trabalho do Garbage Collector (GC).

Listagem 3: Utilizando constantes

public class CreateBoolean {
    
    public static final int COUNT = 1000;
    
    public static void main(String[] args) {
        
        Boolean b1 = null, b2 = null, b3 = null, b4 = null;
        
        final long startTime = System.nanoTime();
        
        for(int i = 0; i < COUNT; i++) {
            b1 = Boolean.TRUE;
            b2 = Boolean.TRUE;
            b3 = Boolean.FALSE;
            b4 = Boolean.FALSE;
        }
        
        final long estimatedTime = System.nanoTime() - startTime;
        
        System.out.println("Compare 1 = " + (b1 == b2));
        System.out.println("Compare 2 = " + (b3 == b4));
        
        System.out.println("Tempo decorrido = " + estimatedTime + " nanosegundos");
    }
}

Saída:

  • Compare 1 = true
  • Compare 2 = true
  • Tempo decorrido = 49075 nanosegundos

Além do tempo decorrido ser bem menor ( 641.950 x 49.075 ), as comparações usando o operador == retornam true, visto que temos referências somente a 2 objetos do tipo Boolean em memória, ao invés dos 4*COUNT objetos do tipo Boolean da versão 1 do programa.

Podemos também usar o método valueOf(), uma vez que ele não cria uma nova instância de Boolean, mas apenas devolve uma das constantes, dependendo do valor da String de entrada.

Recomendação: Utilizar Boolean.TRUE, Boolean.FALSE ou Boolean.valueOf() ao invés de new Boolean().

2) Classes Integer / Short / Long / Byte

Listagem 4: Fragmento da classe Integer da API

    private static class IntegerCache {
        private IntegerCache(){}

        static final Integer cache[] = new Integer[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Integer(i - 128);
        }
    }

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        final int offset = 128;
        if (i >= -128 && i <= 127) { // must cache
            return IntegerCache.cache[i + offset];
        }
        return new Integer(i);
    }

Verificando o código de valueOf(int), temos uma surpresa, pois existe um esquema de cache interno para variáveis inteiras no intervalo de -128 a 127, já que os mesmos são criados como objetos Integer no bloco estático da classe privada IntegerCache. Devido à classe Integer ser imutável, esses objetos pré-definidos podem ser compartilhados sem nenhum problema com qualquer outra classe da aplicação.

Exemplificando, se usarmos esse método duas vezes com o mesmo valor, e este estiver no range, apenas um objeto será devolvido.

Listagem 5: Verificando o esquema de cache

public class IntegerCacheTest {
    
    public static void main(String[] args) {
        
        Integer value1 = Integer.valueOf(1);
        Integer value2 = Integer.valueOf(1);
        Integer value3 = new Integer(1);

        System.out.println("Compare 1 = " + (value1 == value2));
        System.out.println("Compare 2 = " + (value1 == value3));
        
        Integer value4 = Integer.valueOf(159);
        Integer value5 = Integer.valueOf(159);
        Integer value6 = new Integer(159);
        
        System.out.println("Compare 3 = " + (value4 == value5));
        System.out.println("Compare 4 = " + (value4 == value6));
    }
}

Saída:

  • Compare 1 = true
  • Compare 2 = false
  • Compare 3 = false
  • Compare 4 = false

As variáveis value1 e value2 apontam para a mesma referência de memória, devido ao cache interno da classe Integer, por isso o operador == retorna true. Já para value3, um novo objeto é criado, resultando em false a comparação das referências.

Já as variáveis value4 e value5 representam objetos distintos, pois como o valor 159 ultrapassa o range definido, não há cache e um objeto sempre é criado.

Além de Integer, as classes Long, Short e Byte também possuem o mesmo mecanismo de cache para seus respectivos métodos valueOf(), inclusive utilizando o mesmo range (-128 a 127).

Nota: Observe que esses objetos pré-criados são armazenados num array estático final. Isso faz com que eles sejam referenciados praticamente durante toda a execução da JVM, ou seja, o GC não poderá coletá-los, evitando o overhead de desalocação de memória.

Recomendação: Utilizar (Integer|Short|Long|Byte).valueOf(), ao invés de usar o operador new (note que a classe Byte, cujos valores vão de -128 a 127, já possui todo o seu range de valores criados).

3) Classe String

A classe String é uma das classes mais utilizadas em Java, por isso ela é tratada de forma especial pela JVM. Por ser imutável, foi possível implementar o seu conhecido pool interno. Vejamos como ele funciona:

Listagem 6:. Criando objetos String

public class StringTest {
 
    public static void main(String[] args) {
        
        final String str1 = "Hello World";
        final String str2 = "Hello World";
        final String str3 = "Hello " + "World";
        final String str4 = "Hello " + new String("World");
        final String str5 = new String(str1);
        final String str6 = new String("Hello " + "World");
        final String str7 = str4.intern();
        
        System.out.println("Compare 1 = " + (str1 == str2));
        System.out.println("Compare 2 = " + (str1 == str3));
        System.out.println("Compare 3 = " + (str1 == str4));
        System.out.println("Compare 4 = " + (str1 == str5));
        System.out.println("Compare 5 = " + (str1 == str6));
        System.out.println("Compare 6 = " + (str1 == str7));
    }
}

Saída:

  • Compare 1 = true
  • Compare 2 = true
  • Compare 3 = false
  • Compare 4 = false
  • Compare 5 = false
  • Compare 6 = true

Em Java, todas as Strings Literais são armazenadas num pool interno pela JVM, para economizar memória. Por isso o operador == retorna true na comparação entre a str1 e str2.

Perceba, na comparação 2, que o Java consegue detectar que a concatenação de duas literais, no caso “Hello “ + “World” irá resultar numa String que já está no pool, portanto apenas uma referência será mantida (“Hello World” literal), e a comparação retorna true.

O mesmo truque não funciona quando se usa new String(str1) ou concatenação de um literal com um objeto String ("Hello " + new String("World")). Nesse caso, novos objetos são criados e os mesmos não vão para o pool.

Por fim, podemos aplicar o método intern a um objeto String, fazendo com que o retorno do método seja:

  • Se a String não existe no pool, a mesma será adicionada e a referência que está no pool é devolvida
  • Caso já haja uma representação de String similar no pool interno, uma referência a String desse pool será devolvida.

Ou seja, uma vez aplicado intern, é garantido que a String retornada estará no pool interno.

Uma questão muito importante é que, quando aplicamos: String x = new String(“x”);

Estamos criando 2 objetos! Primeiro a String literal “x”, que irá para o pool interno. Depois o próprio objeto criado por new. Por isso, se comparamos “x” == x, o retorno será false, pois são referências para objetos diferentes.

Recomendação: Utilize, quando possível, String literais ao invés de new.

A classe String também nos dá alguns dicas de otimização, por exemplo:

Listagem 7: String hashCode


    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /**
     * Returns a hash code for this string. The hash code for a
     * <code>String</code> object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using <code>int</code> arithmetic, where <code>s[i]</code> is the
     * <i>i</i>th character of the string, <code>n</code> is the length of
     * the string, and <code>^</code> indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0) {
            int off = offset;
            char val[] = value;
            int len = count;

            for (int i = 0; i < len; i++) {
                h = 31*h + val[off++];
            }
            hash = h;
        }
        return h;
    }

Quando o método é chamado pela primeira vez, o valor do hash code será calculado e armazenado no atributo de classe hash. As chamadas subsequentes não precisarão recalcular hash novamente (se ele for diferente de 0), economizando processamento.

Assim, ao criar novas classes e implementar hashCode(), pode-se optar por essa estratégia, a fim de economizar processamento, se for necessário uma abordagem mais agressiva de desempenho.

4) Cuidado com Auto-Boxing

Listagem 8: Comparação entre tipo primitivo e Objeto

public class AutoBoxingTest {

    public static final int COUNT = 100000;    
    
    public static void main(String[] args) {
        
        long startTime = System.nanoTime();        
        Integer value1 = 10;        
        for (int i = 0; i < COUNT; i++) {
            value1 += i;
        }
        long estimatedTime = System.nanoTime() - startTime;
        System.out.println("Tempo decorrido = " + estimatedTime + " nanosegundos");
        
        startTime = System.nanoTime();        
        int value2 = 10;        
        for (int i = 0; i < COUNT; i++) {
            value2 += i;
        }        
        estimatedTime = System.nanoTime() - startTime;
        
        System.out.println("Tempo decorrido = " + estimatedTime + " nanosegundos");
    }
}

Saída:

  • Tempo decorrido = 2649183 nanosegundos
  • Tempo decorrido = 75948 nanosegundos

Lidar com tipos primitivos é muito mais rápido do que com suas respectivas classes Wrappers. Devido ao esquema de Auto-Boxing, cada chamada a “value1 += i” irá resultar na criação de um novo objeto, aumentando consideravelmente o tempo de processamento.

5) Evitar testes com NULL quando não necessário

Dependendo das assertivas de sua rotina, o teste para comparar um valor NULL pode ser desnecessário:

Listagem 9: Testes condicionais desnecessários

public class NullTest {

    public static void main(String[] args) {
   
        String x = null;
        
        // x != null desnecessário, pois equals ao receber null retorna false
        if(x != null && "marcelo".equals(x)) {
            System.out.println("Hello Marcelo");
        }
        
        // x != null desnecessário, pois instanceof retorna false quando o objeto é null
        if(x != null && x instanceof String) {
            System.out.println("x is String");
        }
        
        // x != null desnecessário, pois valueOf retorna "null" quando objeto é null
        if(x != null && String.valueOf(x).equals("123")) {
            System.out.println("x is String");
        }        
        
        if(!"marcelo".equals(x)) {
            System.out.println("Hello Strange");
        }
        
        if(!(x instanceof String)) {
            System.out.println("x???");
        }        
        
        if(String.valueOf(x).equals("null")) {
            System.out.println("x is null");
        }        
    }    
}

Utilizando StringBuffer/StringBuilder ao invés de concatenação de Strings em tempo de execução

Listagem 10: Comparação entre concatenação de Strings

public class StringConcatenationTest {

    public static final int COUNT = 10000;    
    
    public static void main(String[] args) {
        
        long startTime = System.nanoTime();
        String value = "INICIO";
        for (int i = 0; i < COUNT; i++) {
            value += "xy";
        }
        long estimatedTime = System.nanoTime() - startTime;
        System.out.println("Tempo decorrido [CONCATENACAO] = " + estimatedTime + " nanosegundos");
        
        startTime = System.nanoTime();        
        StringBuffer value2 = new StringBuffer("INICIO");
        for (int i = 0; i < COUNT; i++) {
            value2.append("xy");
        }
        estimatedTime = System.nanoTime() - startTime;
        
        System.out.println("Tempo decorrido [StringBuffer] = " + estimatedTime + " nanosegundos");
        
        startTime = System.nanoTime();        
        StringBuilder value3 = new StringBuilder("INICIO");
        for (int i = 0; i < COUNT; i++) {
            value3.append("xy");
        }
        estimatedTime = System.nanoTime() - startTime;
        
        System.out.println("Tempo decorrido [StringBuilder] = " + estimatedTime + " nanosegundos");        
    }
}

Saída:

  • Tempo decorrido [CONCATENACAO] = 475657601 nanosegundos
  • Tempo decorrido [StringBuffer] = 1081924 nanosegundos
  • Tempo decorrido [StringBuilder] = 657643 nanosegundos

Usar StringBuffer/StringBuilder é MUITO mais eficaz do que concatenação de Strings usando variáveis em tempo de execução.

O StringBuilder é também mais rápido que o StringBuffer, pois não usa sincronização. A questão a ser levantada é que, por ser thread-safe, o StringBuffer poderia ser usado ao invés do StringBuilder, mesmo ao custo de uma pequena perda de desempenho (que não é tão grande assim), principalmente se você usar o StringBuffer como um atributo de classe.

Listagem 11: Classe thread-unsafe

public class UnsafeClass {
    
    private final StringBuilder error = new StringBuilder();
    
    public void addMsgError(final String msgError) {
        this.error.append(msgError);
    }
    
    public String getError() {
        return error.toString();
    }
}  

Por exemplo, se uma instância da classe UnsafeClass for compartilhada por mais de uma thread, e ambas chamarem addMsgError, pode haver a geração de estado inconsistente. Para evitar isso, o StringBuffer deve ser usado.

Ambas as classes StringBuilder e StringBuffer herdam (no Java 6) a classe abstrata AbstractStringBuilder, que é thread-unsafe. A única diferença é que, no método append da classe StringBuffer, a palavra synchronized é utilizada.

Listagem 12: StringBuilder e StringBuffer

    // StringBuilder
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

    // StringBuffer
    public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }    

Portanto, só use StringBuilder quando você tiver certeza que o método/classe for thread-safe, ou que a mesma não seja utilizada em ambientes de múltipla thread.

Agora, no caso de simples concatenação de Strings literais em tempo de compilação, não há necessidade de se usar StringBuilder / StringBuffer, pois o próprio compilador irá substituir a concatenação por chamadas a StringBuilder.

Listagem 13: StringBuilder vs Concatenação em tempo de Compilação

public class StringConcatenationTest2 {

    public static final int COUNT = 10000;    
    
    public static void main(String[] args) {
        
        long startTime = System.nanoTime();        
        for (int i = 0; i < COUNT; i++) {
            String value = "INICIO";
            value = "xy" + "wz";
        }
        long estimatedTime = System.nanoTime() - startTime;
        System.out.println("Tempo decorrido [CONCATENACAO] = " + estimatedTime + " nanosegundos");
        
        startTime = System.nanoTime();                
        for (int i = 0; i < COUNT; i++) {
            StringBuilder value2 = new StringBuilder("INICIO");
            value3.append("xy").append("wz");
        }
        estimatedTime = System.nanoTime() - startTime;
        
        System.out.println("Tempo decorrido [StringBuilder] = " + estimatedTime + " nanosegundos");        
    }
}

Saída:

  • Tempo decorrido [CONCATENACAO] = 185739 nanosegundos
  • Tempo decorrido [StringBuilder] = 4276552 nanosegundos

A concatenação de Strings em tempo de compilação foi resolvida pelo compilador de uma forma muito mais performática do que o uso direto de StringBuilder (lembrando que esses resultados foram obtidos no Java 6).

Conclusão

Conhecer bem as classes que usamos diariamente pode render alguns níveis de otimização no código, principalmente em aplicações onde o desempenho e tempo de resposta são requisitos fundamentais.

Na grande maioria das aplicações, otimizações desse nível podem ser desnecessárias, mas, para nós desenvolvedores, analisar o código fonte da API do Java pode ser uma ótima fonte de conhecimento e aprendizado.

Os fontes analisados foram do openjdk-6-src-b27-26_oct_2012.tar.gz, cujo download pode ser obtido em: http://openjdk.java.net/projects/jdk6.

Obrigado e até mais!

Referências 1:

Referências 2: (Cuidado com micro-benchmarcks)


Leia/assista também:

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?