Concatenación de cadenas: operador concat () vs "+"

499

Suponiendo las cadenas a y b:

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

Debajo del capó, ¿son lo mismo?

Aquí está concat descompilado como referencia. También me gustaría poder descompilar el +operador para ver qué hace.

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
fuente
3
No estoy seguro de que +se pueda descompilar.
Galen Nare
1
Use javap para desmontar un archivo de clase Java.
Hot Licks
Debido a la 'inmutabilidad' probablemente deberías estar usando StringBuffero StringBuilder- (hilo inseguro por lo tanto más rápido, en su lugar
Ujjwal Singh

Respuestas:

560

No, no del todo.

En primer lugar, hay una ligera diferencia en la semántica. Si aes así null, a.concat(b)arroja un NullPointerExceptionpero a+=btratará el valor original de acomo si lo fuera null. Además, el concat()método solo acepta Stringvalores, mientras que el +operador convertirá silenciosamente el argumento en una Cadena (utilizando el toString()método para objetos). Entonces, el concat()método es más estricto en lo que acepta.

Para mirar debajo del capó, escriba una clase simple con a += b;

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

Ahora desmonte con javap -c(incluido en Sun JDK). Debería ver una lista que incluye:

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

Entonces, a += bes el equivalente de

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

El concatmétodo debería ser más rápido. Sin embargo, con más cadenas, el StringBuildermétodo gana, al menos en términos de rendimiento.

El código fuente de Stringy StringBuilder(y su clase base de paquete privado) está disponible en src.zip del Sun JDK. Puede ver que está creando una matriz de caracteres (redimensionando según sea necesario) y luego tirándola cuando cree la final String. En la práctica, la asignación de memoria es sorprendentemente rápida.

Actualización: como señala Pawel Adamski, el rendimiento ha cambiado en HotSpot más reciente. javactodavía produce exactamente el mismo código, pero el compilador de bytecode hace trampa. Las pruebas simples fallan por completo porque se desecha todo el cuerpo del código. La suma System.identityHashCode(no String.hashCode) muestra que el StringBuffercódigo tiene una ligera ventaja. Sujeto a cambios cuando se lance la próxima actualización, o si usa una JVM diferente. De @lukaseder , una lista de intrínsecos de HotSpot JVM .

Tom Hawtin - tackline
fuente
44
@HyperLink Puede ver el código usando javap -cuna clase compilada que lo usa. (. Oh, como en la respuesta Sólo tiene que interpretar el código de bytes de desmontaje, que no debería ser tan difícil.)
Tom Hawtin - tackline
1
Puede consultar las especificaciones de JVM para comprender los códigos de bytes individuales. Las cosas a las que le gustaría hacer referencia están en el capítulo 6. Un poco oscuro, pero puede entenderlo con bastante facilidad.
Hot Licks
1
Me pregunto por qué el compilador de Java usa StringBuilderincluso cuando une dos cadenas. Si se Stringincluyen métodos estáticos para concatenar hasta cuatro cadenas, o todas las cadenas de un String[]código, podría agregar hasta cuatro cadenas con dos asignaciones de objetos (el resultado Stringy su respaldo char[], ninguno redundante), y cualquier número de cadenas con tres asignaciones ( el String[], el resultado Stringy el respaldo char[], siendo solo el primero redundante). Tal como está, el uso StringBuilderrequerirá en el mejor de los casos cuatro asignaciones, y requerirá copiar cada carácter dos veces.
supercat
Esa expresión, a + = b. ¿No significa: a = a + b?
Señor más venerable
3
Las cosas han cambiado desde que se creó esta respuesta. Por favor lea mi respuesta a continuación.
Paweł Adamski el
90

Niyaz es correcto, pero también vale la pena señalar que el compilador Java puede convertir el operador especial + en algo más eficiente. Java tiene una clase StringBuilder que representa una cadena mutable no segura para subprocesos. Al realizar un montón de concatenaciones de cadenas, el compilador de Java convierte silenciosamente

String a = b + c + d;

dentro

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

que para cadenas grandes es significativamente más eficiente. Hasta donde yo sé, esto no sucede cuando usas el método concat.

Sin embargo, el método concat es más eficiente cuando se concatena una cadena vacía en una cadena existente. En este caso, la JVM no necesita crear un nuevo objeto String y simplemente puede devolver el existente. Consulte la documentación de concat para confirmar esto.

Entonces, si está súper preocupado por la eficiencia, debe usar el método concat al concatenar cadenas posiblemente vacías y usar + de lo contrario. Sin embargo, la diferencia de rendimiento debería ser insignificante y probablemente nunca debería preocuparse por esto.

Eli Courtwright
fuente
concat de hecho no hace eso. He editado mi publicación con una descompilación del método concat
shsteimer
10
de hecho lo hace. Mira las primeras líneas de tu código concat. El problema con concat es que siempre genera un nuevo String ()
Marcio Aguiar
2
@MarcioAguiar: tal vez quieras decir que + siempre genera un nuevo String, como dices, concattiene una excepción cuando concatás un vacío String.
Blaisorblade
45

Ejecuté una prueba similar a @marcio pero con el siguiente bucle en su lugar:

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
}

Solo por si acaso, también lo hice StringBuilder.append(). Cada prueba se ejecutó 10 veces, con 100k repeticiones para cada ejecución. Aquí están los resultados:

  • StringBuildergana sin dudas. El resultado del tiempo de reloj fue 0 para la mayoría de las carreras, y el más largo tardó 16 ms.
  • a += b toma alrededor de 40000 ms (40 s) por cada ejecución.
  • concat solo requiere 10000 ms (10 s) por ejecución.

No he descompilado la clase para ver los elementos internos o ejecutarlo a través del generador de perfiles todavía, pero sospecho que a += bpasa la mayor parte del tiempo creando nuevos objetos StringBuildery luego volviéndolos a convertir String.

ckpwong
fuente
44
El tiempo de creación de objetos realmente importa. Es por eso que en muchas situaciones usamos StringBuilder directamente en lugar de aprovechar el StringBuilder detrás de +.
coolcfan
1
@coolcfan: Cuando +se usa para dos cadenas, ¿hay algún caso en el que el uso StringBuildersea ​​mejor de lo que sería String.valueOf(s1).concat(s2)? ¿Alguna idea de por qué los compiladores no usarían este último [o de lo contrario omitirían la valueOfllamada en casos donde s1se sabe que no es nulo]?
supercat
1
@supercat lo siento, no lo sé. Quizás las personas que están detrás de este azúcar son las mejores para responder esto.
coolcfan
25

La mayoría de las respuestas aquí son de 2008. Parece que las cosas han cambiado con el tiempo. Mis últimos puntos de referencia realizados con JMH muestran que en Java 8 +es aproximadamente dos veces más rápido que concat.

Mi punto de referencia:

@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
fuente
Me pregunto por qué Java Stringnunca incluyó una función estática para formar una cadena concatenando los elementos de a String[]. Usar +para concatenar 8 cadenas usando una función de este tipo requeriría construir y luego abandonar String[8], pero ese sería el único objeto que necesitaría construirse abandonado, mientras que usar a StringBuilderrequeriría construir y abandonar la StringBuilderinstancia y al menos una char[]tienda de respaldo.
supercat
@supercat Se agregaron algunos String.join()métodos estáticos en Java 8, como envoltorios rápidos de sintaxis en toda la java.util.StringJoinerclase.
Ti Strga
@TiStrga: ¿Ha +cambiado el manejo de usar estas funciones?
supercat
@supercat Eso rompería la compatibilidad binaria hacia atrás, así que no. Fue sólo en respuesta a su "¿Por qué encordar nunca incluyó una función estática" comentario: ahora no es una función de este tipo. El resto de su propuesta (refactorizar +para usarlo) requeriría más de lo que los desarrolladores de Java están dispuestos a cambiar, lamentablemente.
Ti Strga
@TiStrga: ¿Hay alguna forma en que un archivo de bytecode de Java pueda indicar "Si la función X está disponible, llámelo; de lo contrario, haga otra cosa" de una manera que pueda resolverse en el proceso de carga de una clase? Generar código con un método estático que puede encadenarse al método estático de Java o bien usar un generador de cadenas si no está disponible parece la solución óptima.
supercat
22

Tom tiene razón al describir exactamente lo que hace el operador +. Crea un temporal StringBuilder, agrega las partes y termina con toString().

Sin embargo, todas las respuestas hasta ahora están ignorando los efectos de las optimizaciones de tiempo de ejecución de HotSpot. Específicamente, estas operaciones temporales se reconocen como un patrón común y se reemplazan con un código de máquina más eficiente en tiempo de ejecución.

@marcio: Has creado un micro-punto de referencia ; con las JVM modernas, esta no es una forma válida de codificar el perfil.

La razón por la que la optimización del tiempo de ejecución es importante es que muchas de estas diferencias en el código, incluso la creación de objetos, son completamente diferentes una vez que HotSpot se pone en marcha. La única forma de saberlo con certeza es perfilando su código in situ .

Finalmente, todos estos métodos son de hecho increíblemente rápidos. Este podría ser un caso de optimización prematura. Si tiene un código que concatena muchas cadenas, la forma de obtener la velocidad máxima probablemente no tenga nada que ver con los operadores que elija y, en cambio, el algoritmo que está utilizando.

Jason Cohen
fuente
Supongo que por "estas operaciones temporales" te refieres al uso del análisis de escape para asignar objetos de "montón" en la pila donde es probable que sea correcto. Aunque el análisis de escape está presente en HotSpot (útil para eliminar alguna sincronización), no lo creo, es al momento de escribir, u
Tom Hawtin - línea de ataque el
21

¿Qué tal algunas pruebas simples? Usó el siguiente código:

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);
  • La "a + b"versión ejecutada en 2500ms .
  • El a.concat(b)ejecutado en 1200ms .

Probado varias veces. La concat()ejecución de la versión tomó la mitad del tiempo en promedio.

Este resultado me sorprendió porque el concat()método siempre crea una nueva cadena (devuelve un " new String(result)". Es bien sabido que:

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

¿Por qué el compilador no fue capaz de optimizar la creación de cadenas en el código "a + b", sabiendo que siempre resultó en la misma cadena? Podría evitar una nueva creación de cadena. Si no crees en la declaración anterior, prueba tu autoevaluación.

Marcio Aguiar
fuente
Probé en java jdk1.8.0_241 su código, para mí el código "a + b" está dando resultados optimizados. Con concat (): 203 ms y con "+": 113 ms . Supongo que en el lanzamiento anterior no estaba tan optimizado.
Akki
6

Básicamente, hay dos diferencias importantes entre + y el concatmétodo.

  1. Si está utilizando el método concat , entonces solo podrá concatenar cadenas, mientras que en el caso del operador + , también puede concatenar la cadena con cualquier tipo de datos.

    Por ejemplo:

    String s = 10 + "Hello";

    En este caso, la salida debería ser 10Hola .

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

    En el caso anterior, debe proporcionar dos cadenas obligatorias.

  2. La segunda y principal diferencia entre + y concat es que:

    Caso 1: suponga que concaté las mismas cadenas con el operador concat de esta manera

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

    En este caso, el número total de objetos creados en el grupo es 7 como este:

    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy

    Caso 2:

    Ahora voy a concatenar las mismas cadenas a través del operador +

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

    En el caso anterior, el número total de objetos creados son solo 5.

    En realidad, cuando concatenamos las cadenas a través del operador + , mantiene una clase StringBuffer para realizar la misma tarea de la siguiente manera:

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

    De esta manera creará solo cinco objetos.

Entonces, estas son las diferencias básicas entre + y el método concat . Disfruta :)

Deepak Sharma
fuente
Querida, sabes muy bien que cualquier cadena literal tratada como un objeto String en sí mismo que se almacena en el grupo de cadenas. En este caso, tenemos 4 literales de cadena. Por lo tanto, obviamente, al menos 4 objetos deberían crearse en el grupo.
Deepak Sharma
1
No lo creo: String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());impresiones true, lo que significa que "good"no estaba en el grupo de cadenas antes de llamarintern()
fabian
Solo estoy hablando de esta línea String s = "I" + "am" + "good" + "boy"; En este caso, los 4 literales de cadena se mantienen en un grupo. Por lo tanto, se deben crear 4 objetos en el grupo.
Deepak Sharma
4

En aras de la exhaustividad, quería agregar que la definición del operador '+' se puede encontrar en el JLS SE8 15.18.1 :

Si solo una expresión de operando es de tipo String, la conversión de cadena (§5.1.11) se realiza en el otro operando para producir una cadena en tiempo de ejecución.

El resultado de la concatenación de cadenas es una referencia a un objeto String que es la concatenación de las dos cadenas de operandos. Los caracteres del operando de la izquierda preceden a los caracteres del operando de la derecha en la cadena recién creada.

El objeto String se acaba de crear (§12.5) a menos que la expresión sea una expresión constante (§15.28).

Sobre la implementación, JLS dice lo siguiente:

Una implementación puede optar por realizar la conversión y la concatenación en un solo paso para evitar crear y luego descartar un objeto String intermedio. Para aumentar el rendimiento de la concatenación de cadenas repetida, un compilador de Java puede usar la clase StringBuffer o una técnica similar para reducir el número de objetos de cadena intermedios que se crean mediante la evaluación de una expresión.

Para los tipos primitivos, una implementación también puede optimizar la creación de un objeto contenedor mediante la conversión directa de un tipo primitivo a una cadena.

Entonces, a juzgar por 'un compilador de Java puede usar la clase StringBuffer o una técnica similar para reducir', diferentes compiladores podrían producir diferentes códigos de bytes.

dingalapadum
fuente
2

El operador + puede trabajar entre una cadena y un valor de tipo de datos string, char, integer, double o float. Simplemente convierte el valor a su representación de cadena antes de la concatenación.

El operador concat solo se puede hacer con y con cadenas. Comprueba la compatibilidad del tipo de datos y arroja un error, si no coinciden.

Excepto esto, el código que proporcionó hace lo mismo.

Niyaz
fuente
2

No lo creo.

a.concat(b)está implementado en String y creo que la implementación no cambió mucho desde las primeras máquinas java. La +implementación de la operación depende de la versión y el compilador de Java. Actualmente +se implementa utilizando StringBufferpara hacer la operación lo más rápido posible. Quizás en el futuro esto cambie. En versiones anteriores de Java, la +operación en Strings era mucho más lenta ya que producía resultados intermedios.

Supongo que +=se implementa usando +y optimizado de manera similar.

Bartosz Bierkowski
fuente
77
"Actualmente + se implementa utilizando StringBuffer" False It's StringBuilder. StringBuffer es el subproceso seguro de StringBuilder.
Frederic Morin
1
Solía ​​ser StringBuffer antes de Java 1.5, ya que esa era la versión cuando se introdujo StringBuilder por primera vez.
ccpizza
0

Cuando se usa +, la velocidad disminuye a medida que aumenta la longitud de la cadena, pero cuando se usa concat, la velocidad es más estable, y la mejor opción es usar la clase StringBuilder que tiene velocidad estable para hacerlo.

Supongo que puedes entender por qué. Pero la mejor manera de crear cadenas largas es usar StringBuilder () y append (), ya que cualquiera de las dos velocidades será inaceptable.

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