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

Asumiendo String a y b:

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

Debajo del capó, ¿son lo mismo?

Aquí está concat descomstackdo como referencia. Me gustaría poder descomstackr también el operador + para ver qué hace eso.

 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); } } 

No, no del todo

En primer lugar, hay una ligera diferencia en semántica. Si a es null , entonces a.concat(b) arroja una NullPointerException pero a+=b tratará el valor original de a como si fuera null . Además, el método concat() solo acepta valores de String mientras que el operador + convertirá silenciosamente el argumento en una cadena (utilizando el método toString() para objetos). Entonces el método concat() es más estricto en lo que acepta.

Para mirar debajo del capó, escribe 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."":()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 += b es el equivalente de

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

El método concat debe ser más rápido. Sin embargo, con más cadenas, el método StringBuilder gana, al menos en términos de rendimiento.

El código fuente de String y StringBuilder (y su clase base de paquete privado) está disponible en src.zip de Sun JDK. Puede ver que está creando una matriz de caracteres (redimensionando según sea necesario) y luego tirándola cuando cree la String final. 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. javac aún produce exactamente el mismo código, pero el comstackdor de códigos de bytes hace trampa. Las pruebas simples fallan por completo porque se descarta todo el código. Sumando System.identityHashCode (no String.hashCode ) muestra que el código de StringBuffer tiene una ligera ventaja. Sujeto a cambios cuando se lanza la próxima actualización, o si usa una JVM diferente. Desde @lukaseder , una lista de intrusos de HotSpot JVM .

Niyaz tiene razón, pero también vale la pena señalar que el operador especial + puede convertirse en algo más eficiente por el comstackdor de Java. Java tiene una clase StringBuilder que representa una Cadena mutable no segura para subprocesos. Al realizar un conjunto de concatenaciones de cadenas, el comstackdor 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 ocurre cuando usas el método concat.

Sin embargo, el método concat es más eficiente al concatenar 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.

Por lo tanto, si está muy preocupado por la eficiencia, entonces debe usar el método concat al concatenar Cadenas posiblemente vacías, y usar + de lo contrario. Sin embargo, la diferencia en el rendimiento debería ser insignificante y probablemente nunca deberías preocuparte por esto.

Ejecuté una prueba similar a @marcio pero con el siguiente ciclo 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 lancé StringBuilder.append() . Cada prueba se ejecutó 10 veces, con 100k repeticiones para cada ejecución. Aquí están los resultados:

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

No he descomstackdo la clase para ver las partes internas o ejecutarla a través de Profiler todavía, pero sospecho que a += b pasa la mayor parte del tiempo creando nuevos objetos de StringBuilder y luego convirtiéndolos de nuevo a String .

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

Sin embargo, todas las respuestas hasta el momento ignoran 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-benchmark ; con las JVM modernas esta no es una forma válida de crear un código de perfil.

La razón por la cual 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 se inicia HotSpot. La única forma de saber 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 cadenas de caracteres, la forma de obtener la velocidad máxima probablemente no tiene nada que ver con los operadores que elija y, en cambio, con el algoritmo que está utilizando.

¿Qué tal unas simples pruebas? 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 versión "a + b" ejecutada en 2500ms .
  • El a.concat(b) ejecutado en 1200ms .

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

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

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

¿Por qué el comstackdor no era capaz de optimizar la creación de cadenas en el código "a + b", sabiendo que siempre producía la misma cadena? Podría evitar una nueva creación de cadenas. Si no cree en la statement anterior, pruébela usted mismo.

La mayoría de las respuestas aquí son de 2008. Parece que las cosas han cambiado con el tiempo. Mis últimos puntos de referencia hechos con JMH muestran que en Java 8 + es alrededor de 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 

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

  1. Si está utilizando el método concat 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: Supongamos que hago coincidir 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 son 7 como este:

     I am good boy Iam Iamgood Iamgoodboy 

    Caso 2

    Ahora voy a concatinate 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 es solo 5.

    En realidad, cuando concatenamos las cadenas mediante el 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 forma creará solo cinco objetos.

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

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

Si solo una expresión de operando es de tipo String, entonces 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).

Acerca de la implementación, el 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 boost el rendimiento de la concatenación repetida de cadenas, un comstackdor Java puede usar la clase StringBuffer o una técnica similar para reducir el número de objetos String intermedios que se crean mediante la evaluación de una expresión.

Para tipos primitivos, una implementación también puede optimizar la creación de un objeto contenedor convirtiendo directamente de un tipo primitivo a una cadena.

Así que a juzgar por el ‘un comstackdor de Java puede usar la clase StringBuffer o una técnica similar para reducir’, diferentes comstackdores podrían producir diferentes códigos de bytes.

El operador + puede trabajar entre una cadena y un valor de tipo de datos de cadena, char, entero, doble o flotante. Simplemente convierte el valor en su representación de cadena antes de la concatenación.

El operador concat solo se puede ejecutar en 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.

No lo creo.

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

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

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

Supongo que puedes entender por qué. Pero la mejor manera de crear cadenas largas es usar StringBuilder () y append (), ya sea que la velocidad sea inaceptable.