Introdução
Durante o desenvolvimento de software, é comum a necessidade de armazenar uma sequência de caracteres em uma variável. Em Java, esse armazenamento é feito por meio da classe String, porém assim como em outras linguagens, no Java as Strings são constantes e uma vez criada, seu valor não pode ser modificado. Isso significa que a cada concatenação efetuada (operador +) uma nova String é gerada.
A princípio se preocupar com isso parece besteira, ou até mesmo uma otimização prematura, no entanto é importante ressaltar que essa operação dentro de uma iteração é muito custosa, a solução para isso está nas classes StringBuffer e StringBuilder e à veremos nos próximos tópicos, bem como alguns testes de desempenho.
String
Conforme citado na introdução, String é a classe indicada para o armazenamento de uma sequência de caracteres, são também imutáveis e ao concatenar duas Strings uma nova String é gerada. Para entender melhor a frase anterior, vamos escrever um código em Java que concatena duas Strings, para em seguida analisarmos o bytecode gerado.
Começaremos com um código bem simples, onde uma variável s recebe duas Strings “a” e “b”, veja a Listagem 1.
Listagem 1 – Concantenação simples.
public class StringConcat {
public static void main(String[] args) {
String s = "a" + "b";
}
}
Ao analisar o bytecode gerado (Listagem 2) é possível notar que o compilador otimizou o código e não gerou uma nova String para efetuar a concatenação. Repare na linha 0 (Listagem 2) do método main, onde a String ab é gerada diretamente.
Listagem 2 – Bytecode gerado a partir do código da Listagem 1.
public class StringConcat {
public artigo1.StringConcat();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #16 // String ab
2: astore_1
3: return
}
Vamos então ampliar o exemplo, para forçar o compilador a gerar uma nova String, veja o código da Listagem 3.
Listagem 3 – Forçando o compilador a gerar uma nova String.
public class StringConcat {
public static void main(String[] args) {
String a = "a";
String b = "b";
String s = a + b;
}
}
Repare agora nas linhas 0 e 3 do método main da Listagem 4, as Strings a e b são geradas respectivamente, como esperado. Em seguida entre as linhas 6 e 14, é onde o problema se localiza, pois entre essas linhas uma nova instância da classe StringBuilder (essa classe é explicada no próximo tópico) é criada e inicializada com o valor da String a, posteriormente na linha 18 a String b é concatenada por meio do método append() e na sequência o valor é atribuído (linha 24) à String s com o método toString (linha 21).
Listagem 4 – Bytecode gerado a partir da Listagem 3.
public class StringConcat {
public artigo1.StringConcat();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #16 // String a
2: astore_1
3: ldc #18 // String b
5: astore_2
6: new #20 // class java/lang/StringBuilder
9: dup
10: aload_1
11: invokestatic #22 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
14: invokespecial #28 // Method java/lang/StringBuilder."":(Ljava/lang/String;)V
17: aload_2
18: invokevirtual #31 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: return
}
Agora imagine o quão custoso é essa sequência de passos sendo repetida inúmeras vezes dentro de uma iteração. Relembre que, a cada nova iteração, uma instância da classe StringBuilder é criada, o valor é concatenado por meio do método append() e em seguida o valor é armazenado na String desejada.
Para melhor ilustrar esse cenário, vamos a um exemplo simples, suponha que seja necessário gerar um arquivo CSV, no qual cada linha contém algumas informações dos clientes com saldo acima de R$ 50,00. Então na Listagem 5 temos a classe Client e na Listagem 6 o código que itera em uma lista de clientes.
Listagem 5 – Classe Client.
public class Client {
private Long id;
private String name;
private String email;
private Integer age;
private Double balance;
// Getters and Setters were omitted...
@Override
public String toString() {
return this.id.toString() + ";" +
this.name + ";" +
this.email + ";" +
this.age.toString() + ";" +
this.balance.toString();
}
}
Listagem 6 – Gerando informações do cliente.
public class DocumentGenerator {
public String getClientInfo(List clients) {
String clientsInfo = "";
for (Client c : clients) {
if (c.getBalance() > Long.valueOf(50)) {
clientsInfo += c.toString() + "n";
}
}
return clientsInfo;
}
}
A Listagem 5 contém a implementação da classe Client, repare que o método toString() foi sobrescrito para simplificar o código que retorna as informações de um cliente. Na Listagem 6 foi escrito o código que itera em uma lista de clientes, a cada iteração as informações de determinado cliente é armazenada em uma String, além disso, ainda é concatenado o caracter “n” na String, obtendo como resultado final uma String onde cada linha contém um cliente.
Dito isso, um cenário de testes foi montado para a execução do código da Listagem 6, ele foi executado em uma lista de clientes com tamanhos distintos, em seguida teve seu tempo de execução contabilizado e apresentados na lista abaixo:
- 100 clientes, tempo de execução: 2 ms;
- 1.000 clientes, tempo de execução: 59 ms;
- 10.000 clientes, tempo de execução: 3188 ms;
- 100.000 clientes, tempo de execução: 427.945 ms;
Repare que o tempo de execução aumenta bastante conforme o tamanho da lista, isso ocorre devido ao fato de que a cada iteração, uma nova instância da classe StringBuilder é gerada apenas para concatenar as Strings, sem contar que a JVM tem que ficar alocando memória para essas novas instâncias e depois o garbage collector terá de retirá-las da memória.
Está claro que ali existe um problema e que muito recurso está sendo gasto desnecessariamente. Felizmente existe solução para esse problema e ela é apresentada no próximo tópico.
StringBuilder
StringBuilder está disponível desde o JDK 1.5 e ao contrário da classe String, ela é mutável, ou seja, uma instância da classe StringBuilder pode ter seu valor modificado, o que acaba gerando um tempo de execução bem menor quando comparado a instâncias da classe String.
Os métodos mais utilizados da classe StringBuilder são append() e insert(). Ambos foram sobrecarregados para aceitarem todos os tipos dados (Integer, int, String, Double, etc), o método append() concatena valores ao final de uma String, ao passo que o método insert() concatena valores em determinada posição. Neste artigo trabalharemos apenas com o método append().
Na Listagem 7, modificamos o método getClientInfo() para concatenar as informações dos usuários por meio do método append() da classe StringBuilder ao invés de efetuar concatenções com o operador +.
Listagem 7 – Usando o método append() da classe StringBuilder.
public class DocumentGenerator {
public String getClientInfo(List clients) {
StringBuilder clientsInfo = new StringBuilder();
for (Client c : clients) {
if (c.getBalance() > Long.valueOf(50)) {
clientsInfo.append(c.toString());
clientsInfo.append("n");
}
}
return clientsInfo.toString();
}
}
Feito isso, vamos rodar os nossos testes assim como fizemos no tópico anterior e comparar o resultado obtido:
- 100 clientes, tempo de execução: 1 ms;
- 1.000 clientes, tempo de execução: 14 ms;
- 10.000 clientes, tempo de execução: 56 ms;
- 100.000 clientes, tempo de execução: 137 ms;
O resultado é no mínimo surpreendente, com poucos dados a diferença é praticamente imperceptível, vide resultados quando a lista contém 100 ou até mesmo 1.000 clientes, mas conforme a lista cresce a discrepância é muito alta. Enquanto que ao concatenar 100.000 Strings com o operador + obtemos o tempo de execução 427.945 ms, ao concatenar com o método append() da classe StringBuilder o tempo de execução foi de apenas 137 ms.
StringBuffer
E finalmente a classe StringBuffer, ela está disponível deste o JDK 1.0, sendo inclusive mais antiga que a classe StringBuilder. A classe StringBuffer é indicada para trechos de código que devem ser thread safe, ou seja, use a classe StringBuffer quando sua aplicação possui mais de uma thread compartilhando a mesma String, dessa forma você não precisará se preocupar em ficar bloqueando suas threads antes de escrever.
No parágrafo anterior foi usado o termo thread safe. Dizemos que uma classe é thread safe, quando os métodos dessa suposta classe são seguros o suficiente para serem acessados concorrentemente por threads.
O uso desta classe é bastante parecido com o qual vimos na Listagem 7 ao empregarmos a classe StringBuilder, bastando apenas alterar para StringBuffer, para facilitar o entendimento veja a Listagem 8.
Listagem 8 – Usando o método append() da classe StringBuffer.
public class DocumentGenerator {
public String getClientInfo(List clients) {
StringBuffer clientsInfo = new StringBuffer();
for (Client c : clients) {
if (c.getBalance() > Long.valueOf(50)) {
clientsInfo.append(c.toString());
clientsInfo.append("n");
}
}
return clientsInfo.toString();
}
}
Conclusão
Durante o artigo foi apresentado as classes String, StringBuilder e StringBuffer, cada uma foi criada para suprir uma necessidade, ou seja, cada classe dessa, possui um propósito. Vimos que dentro de uma iteração a concatenação de Strings com o operador + é muito custosa, para esses casos deve se usar StringBuilder ou StringBuffer caso exista a necessidade da garantia de sincronização entre threads antes de escrever na String.
Para casos mais simples, não existe problema em usar a classe String, assim como fizemos no método toString() da classe Client na Listagem 5, basta apenas ter bom senso ao optar por uma das classes e evitar de escrever um código complexo em prol de uma otimização insignificante.
E você já conhecia as 3 classes? Por favor, não deixe de compartilhar a sua experiência nos comentários!
Links
- http://docs.oracle.com/javase/1.4.2/docs/api/java/lang/String.html
- http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/StringBuilder.html
- http://docs.oracle.com/javase/1.4.2/docs/api/java/lang/StringBuffer.html
- http://docs.oracle.com/javase/1.5.0/docs/tooldocs/windows/javap.html
- http://c2.com/cgi/wiki?PrematureOptimization
Muito bom! Sucinto e com exemplos, parabéns!