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.