StringBuffer, StringBuilder e String

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 recebe duas Strings “a” “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 ab 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

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s