Revisitando a concatenação de Strings: StringBuilder e StringBuffer
Postado em 13. jun, 2010 por Paulo Silveira 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.
22 Respostas para “Revisitando a concatenação de Strings: StringBuilder e StringBuffer”
Trackbacks/Pingbacks
-
-
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 [...]
ASSINE NOSSO RSS




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.
Washington Botelho
14. jun, 2010
Muito boa a explicação Paulo.
Parabéns! (:
Aécio Costa
15. jun, 2010
como sempre, ótimo post Paulo!
Wilson
15. jun, 2010
Muito boa a explicação!
Parabéns.
Lucas
16. jun, 2010
Muito bom,
Só fiquei curioso em como o String.concat(…) é internamente.
Também com um StringBuilder?
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.
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!
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/
Adams Zago
16. jun, 2010
Ótima explicação… e muitas vezes por conta de pequenos detalhes a performance é prejudicada.
Parabéns.
Maurício Faustino
16. jun, 2010
Ótimo post, bastante curioso!
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…
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.
Giovanni Lima
16. jun, 2010
Great tutorial!
Awesome…
Arvin
16. jun, 2010
Legal o post… parabéns!
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
Felipe Alexandre Rodrigues
16. jun, 2010
A diferença realmente é bruta!
O.O
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.
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.
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
Alexandre Gazola
10. jul, 2010
Muito bom, Paulo!
Keep up the good work!
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).