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

Postado em 13. jun, 2010 por em Java

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.

Tags: , , ,

22 Respostas para “Revisitando a concatenação de Strings: StringBuilder e StringBuffer”

  1. Rafael Carvalho

    14. jun, 2010

    ó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. jun, 2010

    Muito boa a explicação Paulo.

    Parabéns! (:

  3. Aécio Costa

    15. jun, 2010

    como sempre, ótimo post Paulo!

  4. Wilson

    15. jun, 2010

    Muito boa a explicação!

    Parabéns.

  5. Lucas

    16. jun, 2010

    Muito bom,

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

  6. Paulo Silveira

    16. jun, 2010

    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. jun, 2010

    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. jun, 2010

    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. jun, 2010

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

    Parabéns.

  10. Maurício Faustino

    16. jun, 2010

    Ótimo post, bastante curioso!

  11. Luís Carlos Moreira da Costa

    16. jun, 2010

    Excellente post, Paulo!

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

  12. Jairo

    16. jun, 2010

    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. jun, 2010

    Great tutorial!
    Awesome…

  14. Arvin

    16. jun, 2010

    Legal o post… parabéns!

  15. Paulo Silveira

    16. jun, 2010

    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. jun, 2010

    A diferença realmente é bruta!
    O.O

  17. Bruno Laturner

    17. jun, 2010

    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. jun, 2010

    @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. jun, 2010

    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. jul, 2010

    Muito bom, Paulo!

    Keep up the good work!

  21. Luis Roberto

    28. jun, 2011

    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).

Trackbacks/Pingbacks

  1. Arredondamento no Java: do double ao BigDecimal | blog.caelum.com.br - julho 15, 2010

    [...] já que diversos BigDecimals serão instanciados durante a operação, podendo acarretar no mesmo problema de performance do uso de concatenação de Strings. O Donizetti lembrou que esse assunto é bastante discutido no item 48 do Effective [...]

Deixar uma Resposta