Concatenação de cadeias: operador concat () vs "+"

499

Assumindo as seqüências a e b:

a += b
a = a.concat(b)

Sob o capô, eles são a mesma coisa?

Aqui está concat descompilado como referência. Eu gostaria de poder descompilar o +operador também para ver o que isso faz.

public String concat(String s) {

    int i = s.length();
    if (i == 0) {
        return this;
    }
    else {
        char ac[] = new char[count + i];
        getChars(0, count, ac, 0);
        s.getChars(0, i, ac, count);
        return new String(0, count + i, ac);
    }
}
shsteimer
fonte
3
Não sei se +pode ser descompilado.
Galen Nare
1
Use javap para desmontar um arquivo de classe Java.
Hot Licks
Devido à 'imutabilidade', você provavelmente deveria estar usando StringBufferou StringBuilder- (thread inseguro, portanto, mais rápido)
Ujjwal Singh

Respostas:

560

Não, não exatamente.

Em primeiro lugar, há uma pequena diferença na semântica. Se afor null, então a.concat(b)lança um NullPointerExceptionmas a+=btratará o valor original acomo se fosse null. Além disso, o concat()método aceita apenas Stringvalores enquanto o +operador silenciosamente converte o argumento em uma String (usando o toString()método para objetos). Portanto, o concat()método é mais rigoroso no que aceita.

Para olhar por baixo do capô, escreva uma classe simples com a += b;

public class Concat {
    String cat(String a, String b) {
        a += b;
        return a;
    }
}

Agora desmonte com javap -c(incluído no Sun JDK). Você deve ver uma listagem incluindo:

java.lang.String cat(java.lang.String, java.lang.String);
  Code:
   0:   new     #2; //class java/lang/StringBuilder
   3:   dup
   4:   invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V
   7:   aload_1
   8:   invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   11:  aload_2
   12:  invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   15:  invokevirtual   #5; //Method java/lang/StringBuilder.toString:()Ljava/lang/    String;
   18:  astore_1
   19:  aload_1
   20:  areturn

Então, a += bé o equivalente a

a = new StringBuilder()
    .append(a)
    .append(b)
    .toString();

O concatmétodo deve ser mais rápido. No entanto, com mais seqüências, o StringBuildermétodo vence, pelo menos em termos de desempenho.

O código fonte Stringe StringBuilder(e sua classe base privada do pacote) está disponível no src.zip do Sun JDK. Você pode ver que está construindo uma matriz de caracteres (redimensionando conforme necessário) e depois descartando-a quando cria a final String. Na prática, a alocação de memória é surpreendentemente rápida.

Atualização: Como observa Pawel Adamski, o desempenho mudou no HotSpot mais recente. javacainda produz exatamente o mesmo código, mas o compilador de bytecode engana. O teste simples falha completamente porque todo o corpo do código é jogado fora. A soma System.identityHashCode(não String.hashCode) mostra que o StringBuffercódigo tem uma pequena vantagem. Sujeito a alterações quando a próxima atualização for lançada ou se você usar uma JVM diferente. Em @lukaseder , uma lista de intrínsecas do HotSpot JVM .

Tom Hawtin - linha de orientação
fonte
4
@ HyperLink Você pode ver o código usando javap -cuma classe compilada que o utiliza. (. Oh, como na resposta Você só precisa interpretar o bytecode desmontagem, o que não deve ser tão difícil.)
Tom Hawtin - tackline
1
Você pode consultar a especificação da JVM para entender os bytecodes individuais. O material que você gostaria de referenciar está no capítulo 6. Um pouco obscuro, mas você pode entender o essencial facilmente.
Hot Licks
1
Gostaria de saber por que o compilador Java usa StringBuildermesmo quando une duas strings? Se Stringincluíssem métodos estáticos para concatenar até quatro strings, ou todas as strings em a String[], o código poderia anexar até quatro strings com duas alocações de objeto (o resultado Stringe seu respaldo char[], nenhum redundante) e qualquer número de strings com três alocações ( o String[], o resultado Stringe o suporte char[], sendo apenas o primeiro redundante). Como é, o uso StringBuilderexigirá, na melhor das hipóteses, quatro alocações e a cópia de cada caractere duas vezes.
Supercat 2/14
Essa expressão, a + = b. Isso não significa: a = a + b?
mais venerável senhor
3
As coisas mudaram desde quando essa resposta foi criada. Por favor, leia minha resposta abaixo.
Paweł Adamski
90

Niyaz está correto, mas também vale a pena notar que o operador + especial pode ser convertido em algo mais eficiente pelo compilador Java. Java possui uma classe StringBuilder que representa uma String mutável e não segura para threads. Ao executar várias concatenações de String, o compilador Java converte silenciosamente

String a = b + c + d;

para dentro

String a = new StringBuilder(b).append(c).append(d).toString();

que para cordas grandes é significativamente mais eficiente. Tanto quanto eu sei, isso não acontece quando você usa o método concat.

No entanto, o método concat é mais eficiente ao concatenar uma String vazia em uma String existente. Nesse caso, a JVM não precisa criar um novo objeto String e pode simplesmente retornar o existente. Consulte a documentação da concat para confirmar isso.

Portanto, se você está super preocupado com a eficiência, deve usar o método concat ao concatenar cadeias possivelmente vazias e usar + caso contrário. No entanto, a diferença de desempenho deve ser insignificante e você provavelmente nunca deve se preocupar com isso.

Eli Courtwright
fonte
A informação concat não faz isso. Eu editei o meu post com uma descompilação do método concat
shsteimer
10
de fato ele faz. Veja as primeiras linhas do seu código de concat. O problema com a concatenação é sempre que gera uma nova sequência ()
Marcio Aguiar
2
@MarcioAguiar: talvez você queira dizer que + sempre gera um novo String- como você diz, concattem uma exceção quando concatena um vazio String.
Blaisorblade 15/02
45

Fiz um teste semelhante ao @marcio, mas com o seguinte loop:

String c = a;
for (long i = 0; i < 100000L; i++) {
    c = c.concat(b); // make sure javac cannot skip the loop
    // using c += b for the alternative
}

Apenas por uma boa medida, eu joguei StringBuilder.append()também. Cada teste foi executado 10 vezes, com 100k repetições para cada execução. Aqui estão os resultados:

  • StringBuilderganha as mãos para baixo. O resultado da hora foi 0 na maioria das corridas e a mais longa levou 16ms.
  • a += b leva cerca de 40000ms (40s) para cada execução.
  • concat requer apenas 10000ms (10s) por execução.

Ainda não descompilei a classe para ver os internos ou executá-lo no profiler, mas suspeito que a += bgaste a maior parte do tempo criando novos objetos StringBuildere depois convertendo-os novamente em String.

ckpwong
fonte
4
O tempo de criação do objeto é realmente importante. É por isso que, em muitas situações, usamos o StringBuilder diretamente, em vez de tirar proveito do StringBuilder atrás do +.
Coolcfan
1
@coolcfan: Quando +é usado para duas strings, existem casos em que o uso StringBuilderé melhor do que seria String.valueOf(s1).concat(s2)? Alguma idéia de por que os compiladores não usariam o último [ou omitir a valueOfchamada nos casos em que s1se sabe que não são nulos]?
Supercat
1
@ supercat desculpe, eu não sei. Talvez as pessoas que estão por trás desse açúcar sejam as melhores para responder a isso.
coolcfan
25

A maioria das respostas aqui são de 2008. Parece que as coisas mudaram ao longo do tempo. Meus últimos benchmarks feitos com JMH mostram que no Java 8 +é cerca de duas vezes mais rápido que concat.

Minha referência:

@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
public class StringConcatenation {

    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State2 {
        public String a = "abc";
        public String b = "xyz";
    }

    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State3 {
        public String a = "abc";
        public String b = "xyz";
        public String c = "123";
    }


    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State4 {
        public String a = "abc";
        public String b = "xyz";
        public String c = "123";
        public String d = "!@#";
    }

    @Benchmark
    public void plus_2(State2 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b);
    }

    @Benchmark
    public void plus_3(State3 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b+state.c);
    }

    @Benchmark
    public void plus_4(State4 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b+state.c+state.d);
    }

    @Benchmark
    public void stringbuilder_2(State2 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).toString());
    }

    @Benchmark
    public void stringbuilder_3(State3 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).toString());
    }

    @Benchmark
    public void stringbuilder_4(State4 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).append(state.d).toString());
    }

    @Benchmark
    public void concat_2(State2 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b));
    }

    @Benchmark
    public void concat_3(State3 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b.concat(state.c)));
    }


    @Benchmark
    public void concat_4(State4 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b.concat(state.c.concat(state.d))));
    }
}

Resultados:

Benchmark                             Mode  Cnt         Score         Error  Units
StringConcatenation.concat_2         thrpt   50  24908871.258 ± 1011269.986  ops/s
StringConcatenation.concat_3         thrpt   50  14228193.918 ±  466892.616  ops/s
StringConcatenation.concat_4         thrpt   50   9845069.776 ±  350532.591  ops/s
StringConcatenation.plus_2           thrpt   50  38999662.292 ± 8107397.316  ops/s
StringConcatenation.plus_3           thrpt   50  34985722.222 ± 5442660.250  ops/s
StringConcatenation.plus_4           thrpt   50  31910376.337 ± 2861001.162  ops/s
StringConcatenation.stringbuilder_2  thrpt   50  40472888.230 ± 9011210.632  ops/s
StringConcatenation.stringbuilder_3  thrpt   50  33902151.616 ± 5449026.680  ops/s
StringConcatenation.stringbuilder_4  thrpt   50  29220479.267 ± 3435315.681  ops/s
Paweł Adamski
fonte
Eu me pergunto por que o Java Stringnunca incluiu uma função estática para formar uma string concatenando os elementos de a String[]. Usar +para concatenar 8 strings usando essa função exigiria a construção e o abandono posterior String[8], mas esse seria o único objeto que precisaria ser construído abandonado, enquanto o uso de a StringBuilderexigiria a construção e o abandono da StringBuilderinstância e pelo menos um char[]armazenamento de backup.
supercat
@supercat Alguns String.join()métodos estáticos foram adicionados no Java 8, como wrappers de sintaxe rápidos em toda a java.util.StringJoinerclasse.
Ti Strga 24/01
@ TiStrga: O manuseio do +mudou para usar essas funções?
supercat 24/01
@ supercat Isso quebraria a compatibilidade binária com versões anteriores, então não. Foi apenas em resposta a seu "porquê de Cordas nunca incluiu uma função estática" comentário: agora não é tal função. O restante da sua proposta (refatoração +para usá-la) exigiria mais do que aquilo que os desenvolvedores Java estão dispostos a mudar, infelizmente.
Ti Strga 27/01
@ TiStrga: Existe alguma maneira de um arquivo Java bytecode indicar "Se a função X estiver disponível, chame-o; caso contrário, faça outra coisa" de uma maneira que possa ser resolvida no processo de carregamento de uma classe? Gerar código com um método estático que pode encadear com o método estático de Java ou usar um construtor de strings, se não estiver disponível, parece a solução ideal.
supercat 27/01
22

Tom está correto ao descrever exatamente o que o operador + faz. Ele cria um temporário StringBuilder, anexa as peças e termina com toString().

No entanto, todas as respostas até agora estão ignorando os efeitos das otimizações de tempo de execução do HotSpot. Especificamente, essas operações temporárias são reconhecidas como um padrão comum e são substituídas por códigos de máquina mais eficientes em tempo de execução.

@ marcio: Você criou um micro-benchmark ; com as JVM modernas, essa não é uma maneira válida de codificar o perfil.

A razão pela qual a otimização em tempo de execução é importante é que muitas dessas diferenças de código - mesmo incluindo a criação de objetos - são completamente diferentes quando o HotSpot começa. A única maneira de saber com certeza é criar um perfil do seu código in situ .

Finalmente, todos esses métodos são de fato incrivelmente rápidos. Pode ser um caso de otimização prematura. Se você possui um código que concatena bastante as strings, a maneira de obter velocidade máxima provavelmente não tem nada a ver com quais operadores você escolhe e, em vez disso, com o algoritmo que você está usando!

Jason Cohen
fonte
Eu acho que por "essas operações temporárias" você quer dizer o uso de análise de escape para alocar objetos "heap" na pilha, onde provável estiver correto. Embora a análise de escape esteja presente no HotSpot (útil para remover algumas sincronizações), eu não acredito nisso, é no momento da escrita, u
Tom Hawtin - tackline
21

Que tal alguns testes simples? Utilizou o código abaixo:

long start = System.currentTimeMillis();

String a = "a";

String b = "b";

for (int i = 0; i < 10000000; i++) { //ten million times
     String c = a.concat(b);
}

long end = System.currentTimeMillis();

System.out.println(end - start);
  • A "a + b"versão executada em 2500ms .
  • O a.concat(b)executado em 1200ms .

Testado várias vezes. A concat()execução da versão demorou metade do tempo, em média.

Esse resultado me surpreendeu porque o concat()método sempre cria uma nova string (retorna um " new String(result)". É sabido que:

String a = new String("a") // more than 20 times slower than String a = "a"

Por que o compilador não foi capaz de otimizar a criação de strings no código "a + b", sabendo que sempre resultava na mesma string? Isso poderia evitar uma nova criação de string. Se você não acredita na afirmação acima, teste você mesmo.

Marcio Aguiar
fonte
Eu testei em java jdk1.8.0_241 seu código, para mim o código "a + b" está dando resultados otimizados. Com concat (): 203ms e com "+": 113ms . Eu acho que no lançamento anterior não foi tão otimizado.
Akki 28/04
6

Basicamente, existem duas diferenças importantes entre + e o concatmétodo.

  1. Se você estiver usando o método concat , poderá concatenar seqüências apenas no caso do operador + , também poderá concatenar a sequência com qualquer tipo de dados.

    Por exemplo:

    String s = 10 + "Hello";

    Nesse caso, a saída deve ser 10Olá .

    String s = "I";
    String s1 = s.concat("am").concat("good").concat("boy");
    System.out.println(s1);

    No caso acima, você deve fornecer duas seqüências obrigatórias.

  2. A segunda e principal diferença entre + e concat é que:

    Caso 1: Suponha que eu concorde as mesmas strings com o operador concat dessa maneira

    String s="I";
    String s1=s.concat("am").concat("good").concat("boy");
    System.out.println(s1);

    Nesse caso, o número total de objetos criados no pool é 7 assim:

    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy

    Caso 2:

    Agora vou concatinar as mesmas strings via operador +

    String s="I"+"am"+"good"+"boy";
    System.out.println(s);

    No caso acima, o número total de objetos criados é de apenas 5.

    Na verdade, quando concatenamos as strings via operador + , ele mantém uma classe StringBuffer para executar a mesma tarefa da seguinte maneira: -

    StringBuffer sb = new StringBuffer("I");
    sb.append("am");
    sb.append("good");
    sb.append("boy");
    System.out.println(sb);

    Dessa forma, ele criará apenas cinco objetos.

Então, pessoal, essas são as diferenças básicas entre + e o método concat . Desfrutar :)

Deepak Sharma
fonte
Meu querido, você sabe muito bem que qualquer literal de string tratado como um objeto String armazenado no pool de String. Nesse caso, temos 4 literais de string. Portanto, obviamente, pelo menos 4 objetos devem ser criados no pool.
precisa saber é o seguinte
1
Eu não penso assim: String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());impressões true, o que significa "good"não estava na piscina corda antes de chamarintern()
Fabian
Eu estou falando apenas sobre esta linha String s = "I" + "am" + "good" + "boy"; Nesse caso, todos os 4 são literais de string são mantidos em um pool. Portanto, 4 objetos devem ser criados no pool.
Deepak Sharma
4

Por uma questão de completude, eu gostaria de acrescentar que a definição do operador '+' pode ser encontrada no JLS SE8 15.18.1 :

Se apenas uma expressão de operando for do tipo String, a conversão de string (§5.1.11) será executada no outro operando para produzir uma string em tempo de execução.

O resultado da concatenação de cadeias de caracteres é uma referência a um objeto String que é a concatenação das duas cadeias de operandos. Os caracteres do operando do lado esquerdo precedem os caracteres do operando do lado direito na sequência recém-criada.

O objeto String é criado recentemente (§12.5), a menos que a expressão seja uma expressão constante (§15.28).

Sobre a implementação, o JLS diz o seguinte:

Uma implementação pode optar por executar a conversão e concatenação em uma etapa para evitar a criação e o descarte de um objeto String intermediário. Para aumentar o desempenho da concatenação de seqüência de caracteres repetida, um compilador Java pode usar a classe StringBuffer ou uma técnica semelhante para reduzir o número de objetos String intermediários criados pela avaliação de uma expressão.

Para tipos primitivos, uma implementação também pode otimizar a criação de um objeto wrapper, convertendo diretamente de um tipo primitivo em uma sequência.

Portanto, a julgar pelo 'um compilador Java pode usar a classe StringBuffer ou uma técnica semelhante para reduzir', diferentes compiladores podem produzir códigos de bytes diferentes.

dingalapadum
fonte
2

O operador + pode trabalhar entre uma string e um valor de tipo de dados string, char, inteiro, double ou float. Ele apenas converte o valor em sua representação de string antes da concatenação.

O operador concat pode ser feito apenas com e com strings. Ele verifica a compatibilidade do tipo de dados e gera um erro, se não corresponderem.

Exceto isso, o código que você forneceu faz o mesmo.

Niyaz
fonte
2

Acho que não.

a.concat(b)é implementado em String e acho que a implementação não mudou muito desde as primeiras máquinas java. A +implementação da operação depende da versão e compilador Java. Atualmente +é implementado usando StringBufferpara tornar a operação o mais rápido possível. Talvez no futuro isso mude. Nas versões anteriores da +operação java no Strings, era muito mais lento, pois produzia resultados intermediários.

Eu acho que isso +=é implementado usando +e igualmente otimizado.

Bartosz Bierkowski
fonte
7
"Atualmente + é implementado usando StringBuffer" False É StringBuilder. StringBuffer é o implemento seguro de StringBuilder.
Frederic Morin
1
Costumava ser StringBuffer antes do java 1.5, como era a versão quando o StringBuilder foi introduzido pela primeira vez.
Ccpizza
0

Ao usar +, a velocidade diminui à medida que o comprimento da string aumenta, mas ao usar concat, a velocidade é mais estável e a melhor opção é usar a classe StringBuilder que possui velocidade estável para fazer isso.

Eu acho que você pode entender o porquê. Mas a melhor maneira de criar seqüências longas é usando StringBuilder () e append (), ou a velocidade será inaceitável.

iamreza
fonte
1
usar o operador + equivale a usar o StringBuilder ( docs.oracle.com/javase/specs/jls/se8/html/… )
ihebiheb