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

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

Compartilhe

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()); 
Banner promocional da Alura, com chamada para um evento ao vivo no dia 12 de fevereiro às 18h30, com os dizeres

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.

Veja outros artigos sobre Programação