Revisitando a concatenação de Strings: StringBuilder e StringBuffer

Uma discussão muito antiga que frequentemente aparece no Java é o uso errado da concatenação de Strings, que pode acarretar numa grave perda de performance e trashing de memória. Mas por que?

O problema é muito simples de enxergar. Imagine um laço em que você concatena uma String com todos os números de 0 a 30 mil:

                String numeros = "";
                for (int i = 0; i<30000; i++) {
                        numeros += i;
                }
                System.out.println(numeros.length());

Em um computador bom, isso vai levar vários segundos. Agora vamos verificar o mesmo código usando um StringBuider:

                StringBuilder numeros = new StringBuilder();
                for (int i = 0; i<30000; i++) {
                        numeros.append(i);
                }
                System.out.println(numeros.toString().length());

Rodando esse segundo código, o tempo gasto é irrisório, mal sendo percepitível. Alguns chegam até a dizer que não devemos utilizar o operador +, nem mesmo em operações simples como essas:

String hql = "select u from"; 
hql += " User as u"; 
System.out.println(hql);

Porém este é o caso que o uso do operador + é mais que bem vindo. No fundo, este operador não existe para a JVM, é apenas um syntactic sugar na linguagem e único operator overload do Java. O próprio compilador trata este operador, como podemos verificar no bytecode desse código através do javap -c, resultando no trecho de mneumônicos como este. Logo, o que está acontecendo na verdade é um código como:

System.out.println(new StringBuilder()
     .append("select u from").append(" User as u:").toString());

Em outras palavras, o operador + sempre usa o StringBuilder, o que torna desnecessário evitar o uso do operador + neste caso. Se ele já usa o StringBuilder, por que o código que vimos primeiramente roda tão mais lento com o operador em relação ao StringBuilder puro? Voltando ao primeiro código, ele gera este bytecode, que podemos facilmente perceber que há uma instanciação de um novo StringBuilder a cada iteração do laço, laço o qual está definido entre as instruções 5 e 34 (o goto). Em Java teríamos:

                String numeros = "";
                for (int i = 0; i<30000; i++) {
                        numeros = new StringBuilder()
                             .append(numeros).append(i).toString();
                }
                System.out.println(numeros.length());

Repare que, com um novo StringBuilder sendo instanciado a cada iteração, a String numeros está sendo copiada inteiramente (append(numeros)) toda vez para esse novo objeto, gastando bastante tempo (no final, é um tempo quadrático em relação ao tamanho da String). O nosso segundo código já apresentado é bem mais eficiente: ele cria um StringBuilder uma única vez, fora do laço, e depois invoca o append apenas para as novas partes da String, sem ter de copiar o que já foi previamente processado (resultando em tempo linear).

Por último temos o StringBuffer: é a versão antiga e thread safe do StringBuilder, que era usado antigamente para realizar as operações do +. Como ele usa sincronização, custa um pouco mais caro para executar seus métodos, e foi preterido por essa ser uma situação thread safe.

25 Comentários

  1. Rafael Carvalho 14/06/2010 at 10:45 #

    ótima explicação e exemplo. conheço bastante gente que está começando com Java e precisa dessa explicação.

    abraços.

  2. Washington Botelho 14/06/2010 at 13:22 #

    Muito boa a explicação Paulo.

    Parabéns! (:

  3. Aécio Costa 15/06/2010 at 09:10 #

    como sempre, ótimo post Paulo!

  4. Wilson 15/06/2010 at 09:16 #

    Muito boa a explicação!

    Parabéns.

  5. Lucas 16/06/2010 at 08:24 #

    Muito bom,

    Só fiquei curioso em como o String.concat(…) é internamente.
    Também com um StringBuilder?

  6. Paulo Silveira 16/06/2010 at 08:34 #

    Oi Lucas

    Curiosamente nenhum método da String usa internamente StringBuffer ou StringBuilder. Ela vai sempre criar um novo array de char na mão do tamanho necessário e retornar a nova String (concatenando na unha com System.arraycopy através do método getChars da String)… poderia perfeitamente ter usado um StringBuilder (que o append usa o mesmo getChars da String), mas custaria um pouco mais caro por ter de instanciar esse objeto intermediario que seria rapidamente descartado.

  7. Christian Borges 16/06/2010 at 09:02 #

    Excelente!!!

    Explicação rápida e eficiente! Sempre gostei de trabalhar com Strings e também sempre me preocupo com a performance dos programas que desenvolvo. Com isso essas informações serão de grande utilidade.

    Valeu Paulo!

  8. Angelina Uesato 16/06/2010 at 10:53 #

    Legal o post, recentemente fiz uma comparação da utilização da String, StringBuffer e StringBuilder

    http://www.devfordummies.com/2010/05/performace-faz-a-diferenca-string-stringbuffer-e-stringbuilder/

  9. Adams Zago 16/06/2010 at 11:01 #

    Ótima explicação… e muitas vezes por conta de pequenos detalhes a performance é prejudicada.

    Parabéns.

  10. Maurício Faustino 16/06/2010 at 11:06 #

    Ótimo post, bastante curioso!

  11. Luís Carlos Moreira da Costa 16/06/2010 at 11:37 #

    Excellente post, Paulo!

    Não me lembro no momento mas existe uma API, mais rápida que StringBuffer, StringBuilder…

  12. Jairo 16/06/2010 at 12:25 #

    Se o problema for performance.
    O arrayCopy é até mais que os dois, StringBuilder e StringBuffer.
    Classe: System
    Método: public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
    O único problema seria a complexidade.

  13. Giovanni Lima 16/06/2010 at 12:26 #

    Great tutorial!
    Awesome…

  14. Arvin 16/06/2010 at 12:59 #

    Legal o post… parabéns!

  15. Paulo Silveira 16/06/2010 at 13:08 #

    Oi pessoal

    Sem duvida a performance importa, mas é necessário ficar atento que a diferenca é gritante entre String e StringBuilder/Buffer quando a String é usada de maneira errada. Passar a diante disso, como o Jairo deu a idéia de usar arrayCopy em array de char diretamente, vai ter um ganho minúsculo em percetual, e não em magnitude.

    Agradeço os comentários!

    Paulo

  16. Felipe Alexandre Rodrigues 16/06/2010 at 14:50 #

    A diferença realmente é bruta!
    O.O

  17. Bruno Laturner 17/06/2010 at 02:20 #

    Paulo, tenho que discordar dos resultados do bytecode gerado. Ou pelo menos falar que não é o único resultado possível.

    String hql = “select u from”;
    hql += ” User as u”;

    Usando o compilador da Oracle(na época, JDev 10.1.2), já vi que ele transforma isso direto em

    String hql = “select u from User as u”;

    Mas por regra é isso mesmo, usar construtores de Strings quando for construir Strings dinamicamente.

  18. Paulo Silveira 17/06/2010 at 13:28 #

    @Bruno: Perfeito. Tem razão, eu deveria ter dito que a JLS deixa em aberto as formas de otimizar esse código, e que meu bytecode era específico do Javac do JDK 1.6 da Sun:
    http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.18.1.2

    Mas se você colocar um IF ai para que a concatenação não seja óbvia (if (algumasCoisa) hql += "bla"), nenhum compilador vai conseguir fazer esse truque mais esperto que a Oracle conseguiu.

    E como você mesmo concluiu, não altera o resultado da concatenação: em qualquer compilador usar o += para criar as queries dinâmicas vai ser uma ordem de magnitude mais lenta.

  19. guilherme 22/06/2010 at 11:55 #

    acredito que muita gente que desenvolve a anos não sabia disso, eu não sabia… e achava que sabia muito sobre o StringBuilder, mas sobre o operador +=… essa é nova… muito bom

  20. Alexandre Gazola 10/07/2010 at 16:30 #

    Muito bom, Paulo!

    Keep up the good work!

  21. Luis Roberto 28/06/2011 at 11:31 #

    Ficou uma dúvida: qual o motivo do bytecode sair dessa forma, instanciando sempre dentro do laço? Não cobnsegui enxergar porque ele não otimiza da forma mais inteligente (instanciar fora do laço e dar append dentro).

  22. Fabio da Silva 16/03/2013 at 19:33 #

    Muito bom o artigo, até coloquei no meu blog http://fabiophx.blogspot.com.br/2013/03/concatenacao-de-strings-e-calculo-com.html pois às vezes alguns colegas me perguntam a respeito, agora em vez de explicar irei indicar essa leitura.

  23. Arthur Bezerra 31/07/2015 at 04:30 #

    Ótimo post!! Tirou uma grande dúvida minha. Muito obrigado!

Deixe uma resposta