Questão de performance com concatenação

Olá amigos,

Recentemente fiz um curso de Spring, e como curiosidade o professor Douglas Campos, citou uma coisa muitíssimo interessante: hoje usar o operador + para concatenar string pode ser uma boa opção em grande parte dos casos (a partir da hotspot do Java 6). Porque o compilador vai transformar isto para a concatenação com StringBuilder.
Isto me deixou muito intrigado, e foi procurar alguma referência oficial da Sun para escrever este post, e disseminar esta informação. Encontrei, cumprindo a premissa de quem procura acha!!! Porém!!! Achei a referencia de performance da Sun um pouco antiga, e lá no capitulo sobre imutabilidade ele diz o seguinte:

/disponível em: http://java.sun.com/docs/books/performance/1st_edition/html/JPMutability.fm.html#22028/

String result = ""; for (int i=0; i < 20; i++) { result += getNextString(); }
Concatenating String objects
The javac compiler would automatically transform this to

String result = ""; for (int i=0; i < 20; i++) { result = new StringBuffer().append(result) .append(getNextString()) .toString(); }

Observe que é StringBuffer, mas como o StringBuffer não é mais recomendado pela Oracle/Sun (Sei lá como chamo isto), isto foi atualizado para usar o StringBuilder.
Se alguém tiver uma referência oficial atualizada sobre isto post por favor!

Grande Abraço!

Está correto! A unica questao é que a partir do Java 5 o compilador vai gerar com StringBuilder e não com StringBuffer.

Realmente é preciso pensar com cuidado nas otimizacoes que fazemos com concatenacoes de strings porque no fundo é tudo StringBuilder. Mas esse caso de concatenar num for é um classico onde ainda precisamos escrever o StringBuilder na mao para nao sofrer com a performance.

Repare que quando voce faz o for com + o compilador gera a concatenacao com StringBuilder. Mas como voce atribui o resultado de volta numa String, ele precisa criar um StringBuilder novo a cada iteracao e chamar o toString de volta. O que, claro, não é muito performatico se voce precisar da String so no final do for (repare que o compilador nao tem como saber isso, por isso gera o codigo “ruim”).

Para melhorar a performance, vc precisa fazer o codigo classico de criar o StringBuilder fora do for e depois só chamar o append dentro do laço. E após o for chamar o toString uma unica vez. Aí vai ficar bem mais rapido!

E um ultimo comentario: o compilador transforma concatenacoes de String em StringBuffer/Builder desde sempre (não só Java 6). O que mudou foi que no Java 5 em diante ele deixou de usar StringBuffer e passou a usar StringBuilder.

Boa Sérgio!

Apenas acrescentando, tem um post do paulo silveira (pt-br) e um paper bem interessante a respeito de performance de concatenação de strings (inglês). Vale a leitura!

Agora, note que num loop, o uso do StringBuilder implícito pela VM não criou nada mais otimizado. O loop que você mesmo colocou mostra isso: É criado um stringBuffer a cada iteração, e sobre ele é chamado o toString(), resultando numa nova String. O custo de alocação desse novo objeto ainda existe e, portanto, não houve ganho de performance.

O que seu professor falou, é que para os casos das concatenações simples, isso já está otimizado:

[code]String nome = “Vinicius”;
String sobrenome = “Godoy de Mendonça”;

public void String getSobrenome() {
return nome + " " + sobrenome;
}[/code]

É equivalente a:

public void String getSobrenome() { return new StringBuilder(nome).append(" ").append(sobrenome).toString(); }

Agora, num loop, você ainda precisa criar o StringBuilder manualmente.

Ressaltando o que o ViniGodoy afirmou, façam o seguinte teste:

// Rode este programa com:
//
// java -Xcomp -Xmx128m -cp . TesteAppend
//
// (Se estiver usando o JDK, experimente com:
//
// java -server -Xcomp -Xmx128m -cp . TesteAppend
//

class TesteAppend {
    private static void gerarStrings (String[] strings) {
        for (int i = 0; i < strings.length; ++i) {
            strings[i] = String.valueOf (i);
        }
    }
    private static void teste1 (String[] strings) {
        String result = "";
        for (int i = 0; i < strings.length; ++i) {
            result += strings[i];
        }
        System.out.println (result.length());
    }
    private static void teste2 (String[] strings) {
        StringBuilder sb = new StringBuilder();
        String result;
        for (int i = 0; i < strings.length; ++i) {
            sb.append (strings[i]);
        }
        result = sb.toString();
        System.out.println (result.length());
    }
    private static void teste3 (String[] strings, int alloc) {
        StringBuilder sb = new StringBuilder(alloc);
        String result;
        for (int i = 0; i < strings.length; ++i) {
            sb.append (strings[i]);
        }
        result = sb.toString();
        System.out.println (result.length());
    }
    public static void main(String[] args) {
        long time;
        String[] strings = new String[1000000];
        System.out.println ("Gerando 1 milhão de strings...");
        time = System.currentTimeMillis();
        gerarStrings(strings);
        time = System.currentTimeMillis() - time;
        System.out.println ("Tempo gasto: " + time + " ms");

        System.out.println ("Concatenando 1 milhão de strings usando StringBuilder.append");
        time = System.currentTimeMillis();
        teste2(strings);          
        time = System.currentTimeMillis() - time;
        System.out.println ("Tempo gasto: " + time + " ms");

        System.out.println ("Concatenando 1 milhão de strings usando StringBuilder.append (prealocado)");
        time = System.currentTimeMillis();
        teste3(strings, 6000000);          
        time = System.currentTimeMillis() - time;
        System.out.println ("Tempo gasto: " + time + " ms");

        System.out.println ("Concatenando 1 milhão de strings usando +=");
        time = System.currentTimeMillis();
        teste1(strings);          
        time = System.currentTimeMillis() - time;
        System.out.println ("Tempo gasto: " + time + " ms");
    }
}

E postem aqui os tempos obtidos.

Show de bola pessoal,

Eu realmente havia feito alguns testes com isto, para o caso de concatenação simples sempre utilizei StringBuffer e depois StringBuilder por que não tinha este conhecimento que o compilador fazia este esforço para otimizar (claro que depois desta explicação de vocês podemos definir como proibitivo para os casos que vai concatenar em um looping), me parece que esta otimização é uma coisa pouco conhecida pela comunidade Java. Muito obrigado pelas referências, os testes e a paciência!

Abraços