String, StringBuilder e StringBuffer - Qual devo usar?

No Java podemos representar sequências de caracteres para exibirmos mensagens, armazenarmos informações e fazermos outros usos diversos. E a forma mais popular de se trabalhar com cadeia de caracteres é através das Strings, que são rápidas e eficientes nisso.

A classe String representa uma cadeia de caracteres. Todas as strings literais em um programa Java, assim como "abc", são implementadas como instância de uma classe String. E se tratando de uma classe, a mesma não é um tipo de dado primitivo e sim um TAD (tipo abstrato de dado).

Outras classes que podemos trabalhar também com cadeia de caracteres são as classes StringBuffer e StringBuilder. Mas, qual devemos usar? O objetivo desse artigo é esclarecer esse ponto.

String x StringBuffer x StringBuilder - qual usar?

Entendendo as Strings

A String pertence ao pacote java.lang, assim como StringBuffer e StringBuilder, logo não precisa ser importada em seus programas. Eis algumas maneiras de se criar uma String:

public class Exemplo1 {
    public static void main(String[] args) {
        //Atribuição direta
        String a = "Mensagem 1";
        //Separação entre a instanciação e atribuição
        String b = new String();
        b = "Mensagem 2";
        //Passando um vetor de caracteres de char como argumento
        char msg[] = {'M', 'e', 'n', 's', 'a', 'g', 'e', 'm', ' ', '3'};
        String c = new String(msg);
        //Passando uma sequência de caracteres diretamente como argumento
        String d = new String("Mensagem 4");

        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
        System.out.println(d);
    }
}

A saída será:
Mensagem 1
Mensagem 2
Mensagem 3
Mensagem 4

Até aqui não há nenhum problema em fazer o uso delas. Mas e se quisermos alterar a mensagem de uma String através da concatenação com o operador "+"? Veja:

public class Exemplo2 {
    public static void main(String[] args) {
        String msg = "a";
        msg = msg + "bc";

        System.out.println(msg); 
    }
}

A saída será:
abc

O que aconteceu nesse exemplo foi a instanciação de uma nova String com a sequência de caracteres "a" e "bc", na qual msg faz referência (após deixar de referenciar "a"). Mas "a" ainda está na memória do computador, só que em um outro local. Dentro da String há o seguinte atributo, onde a sequência de caracteres é armazenada:

    private final char value[]; 

Como podemos perceber, não há mesmo como modificar o valor da String já que ele é final, ou seja, é impossível redimensionar o vetor para acomodar mais algum caractere (por isso Strings são tidas como imutáveis). A forma mais apropriada de se realizar essa tarefa seria copiando a String original e concatenando com a String que foi passada, gerando uma nova String mas esse processo pode levar muito tempo e acumular muitos objetos na memória.

Concatenar muitas Strings é uma Má Ideia

Considere o exemplo a seguir e a sua respectiva saída em um Intel Celeron N2808 @ 2x 2.2491GHz com 4 GB de memória RAM:

public class Exemplo3 {

    public static void main(String[] args) {
        String msg = "";
        double x, x0, deltaX;
        
        x0 = System.nanoTime(); //tempo inicial (nanossegundos)
        for (int i = 0; i < 36_600; i++) {
            msg = msg + i;
        }
        x = System.nanoTime(); //tempo final (nanossegundos)
        deltaX = (x - x0)/Math.pow(10, 9); //conversão para segundos
        System.out.printf("Segundos passados: %.0f\n", deltaX);
    }
}

Saída:
Segundos passados: 11

Em sua máquina o resultado pode ser diferente, mas a observação a se fazer é que para 36.600 concatenações foi preciso um tempo considerável, além do uso da memória para guardar cada novo objeto resultante da concatenação.

StringBuilder e StringBuffer

Tanto StringBuilder quanto StringBuffer suportam mutações de caracteres. Vejamos o mesmo código demonstrado anteriormente, só que agora utilizando StringBuffer.

public class Exemplo4 {

    public static void main(String[] args) {
        StringBuffer msg = new StringBuffer();
        double x, x0, deltaX;
        
        x0 = System.nanoTime();
        for (int i = 0; i < 36_600; i++) {
            msg.append(i); //método para concatenação
        }
        x = System.nanoTime();
        deltaX = (x - x0)/Math.pow(10, 9);
        System.out.printf("Segundos passados: %.5f\n", deltaX);
    }
}

Saída:
Segundos passados: 0,01787

O resultado foi 61.556% (sessenta e um mil quinhentos e cinquenta e seis por cento) menor. Com StringBuilder o resultado é de 0,03031 segundos, que também é um valor relativamente muito baixo.

StringBuffer:
Uma thread-safe — isto é, que manipula EDs (estrutura de dados) compartilhadas entre diversas threads de forma segura —, sequência de caracteres mutáveis. Uma StringBuffer é como uma String, mas pode ser modificada, pois o tamanho e conteúdo da sequência podem ser alterados através da chamada de certos métodos.

Explicando de uma maneira simplificada: ela recebe a String, que é passada para o método append, e ele chama o método append da sua super classe (que é abstrata). A lógica é aumentar a capacidade do vetor de caracteres, manter a sequência de caracteres anterior e adicionar esse novo conteúdo a ele (o vetor de char).

StringBuilder:
Uma sequência de caracteres mutáveis. Essa classe fornece uma API compatível com StringBuffer, mas com a não-garantia de sincronização (o que a diferencia da StringBuffer). Essa classe é designada para uso como uma substituta para StringBuffer onde ela será usada por uma única thread (como geralmente acontece).

Sempre que possível essa classe é recomendada, já que é mais rápida que a StringBuffer na maioria das implementações. Tanto a StringBuffer quanto a StringBuilder, se instanciadas sem nenhum conteúdo, iniciam com a capacidade total de 16 caracteres.

Conclusão

A String deve ser usada sempre que sua aplicação exigir poucas concatenações, já que Strings são imutáveis e portanto sempre criarão um novo objeto na memória com o novo conteúdo acrescentado.

StringBuffer e StringBuilder devem ser utilizadas sempre que houver um número elevado de concatenações em sua aplicação, e se precisar converter para String para exibir uma mensagem ou armazenar em algum lugar, basta usar o método toString(). A diferença entre StringBuffer e StringBuilder é a questão da sincronização, portanto a primeira é mais recomendada para se trabalhar com threads.

Referências
JAVA PLATFORM SE 7. StringBuilder. Disponível em <https://docs.oracle.com/javase/7/docs/api/java/lang/StringBuilder.html>. Acesso em 19 abr. 2017.

JAVA PLATFORM SE 7. StringBuffer. Disponível em <https://docs.oracle.com/javase/7/docs/api/java/lang/StringBuffer.html>. Acesso em 19 abr. 2017.

JAVA PLATFORM SE 7. String. Disponível em <https://docs.oracle.com/javase/7/docs/api/java/lang/String.html>. Acesso em 19 abr. 2017.


Para citar esse artigo:

Comentários