Debatíamos Bastian y yo en los comentarios de la entrada sobre FindBugs acerca de la necesidad o no de realizar las concatenaciones de cadenas en Java utilizando las clases String o StringBuilder. Bastian apuntaba que en [1] se decía que el compilador de Sun desde la versión Java 1.5 ya realizaba estas optimizaciones.
Al final la conversación quedó en que como deberes yo haría una prueba, y durante un viaje en autobús la realicé.
El código de ambos tests es el siguiente:
Test con String:
public class LoopsStringConcatenation { public static void main(String[] args) { long t0 = System.currentTimeMillis(); String s = new String(); for (int i=0; i<10000; i++) { s += "a"; } long t = System.currentTimeMillis() - t0; System.out.println(t); } } [/sourcecode] Test con StringBuilder: [sourcecode language="java"] public class LoopsStringBuilder { public static void main(String[] args) { long t0 = System.currentTimeMillis(); StringBuilder s = new StringBuilder(); for (int i=0; i<10000; i++) { s.append("a"); } long t = System.currentTimeMillis() - t0; System.out.println(t); } } [/sourcecode] Si ejecutamos los dos tests, podemos comprobar que el método que utiliza String es bastante más lento que el método que usa StringBuilder (sí, "bastante más lento" va a servir en este caso, no voy a poner números pues esto es una prueba no formal). No obstante, vamos a comprobar las instrucciones de bytecode que se generan en ambos casos, y vamos a ver cuál es la diferencia. Bytecode del test con String: [sourcecode language="java"] 0 invokestatic #16 <java/lang/System.currentTimeMillis> 3 lstore_1 4 new #22 <java/lang/String> 7 dup 8 invokespecial #24 <java/lang/String.<init>> 11 astore_3 12 iconst_0 13 istore 4 15 goto 41 (+26) 18 new #25 <java/lang/StringBuilder> 21 dup 22 aload_3 23 invokestatic #27 <java/lang/String.valueOf> 26 invokespecial #31 <java/lang/StringBuilder.<init>> 29 ldc #34 <a> 31 invokevirtual #36 <java/lang/StringBuilder.append> 34 invokevirtual #40 <java/lang/StringBuilder.toString> 37 astore_3 38 iinc 4 by 1 41 iload 4 43 sipush 10000 46 if_icmplt 18 (-28) 49 invokestatic #16 <java/lang/System.currentTimeMillis> 52 lload_1 53 lsub 54 lstore 4 56 getstatic #44 <java/lang/System.out> 59 lload 4 61 invokevirtual #48 <java/io/PrintStream.println> 64 return
Bytecode del test con StringBuilder:
0 invokestatic #16 <java/lang/System.currentTimeMillis> 3 lstore_1 4 new #22 <java/lang/StringBuilder> 7 dup 8 invokespecial #24 <java/lang/StringBuilder.<init>> 11 astore_3 12 iconst_0 13 istore 4 15 goto 28 (+13) 18 aload_3 19 ldc #25 <a> 21 invokevirtual #27 <java/lang/StringBuilder.append> 24 pop 25 iinc 4 by 1 28 iload 4 30 sipush 10000 33 if_icmplt 18 (-15) 36 invokestatic #16 <java/lang/System.currentTimeMillis> 39 lload_1 40 lsub 41 lstore 4 43 getstatic #31 <java/lang/System.out> 46 lload 4 48 invokevirtual #35 <java/io/PrintStream.println> 51 return
Como vemos, es cierto que el compilador utiliza StringBuilder en vez de String. Sin embargo, la diferencia de rendimiento se debe a que la variable local s es un String, y en cada iteración se invoca a toString() para poder asignarlo, lo que nos crea un nuevo objeto String en cada iteración.
Veamos pues en qué caso esta optimización del compilador es útil.
Si realizamos sucesivas concatenaciones, en distintas sentencias, estamos en el mismo caso que el anterior. El resultado debe ser un String, por lo que se realizar diversas llamadas al método toString() que nos penalizan.
Si concatenamos cadenas en una misma sentencia, siendo éstas variables, entonces el compilador si usa StringBuilder, y en este único caso si es interesante usar el operador + para mejorar la legibilidad.
Como curiosidad, en caso de que sean constantes (por ejemplo, s += “a” + “a” + “a”;) en tiempo de compilación ya se optimiza (en el ejemplo, quedaría bytecode equivalente a s += “aaa”;).
Volviendo a nuestro caso de concatenación de variables en una misma sentencia, sea el siguiente fragmento de código:
String t = "a"; String s = new String(); s += t + t + t;
Genera:
4 ldc #22 <a> 6 astore_3 7 new #24 <java/lang/String> 10 dup 11 invokespecial #26 <java/lang/String.<init>> 14 astore 4 16 new #27 <java/lang/StringBuilder> 19 dup 20 aload 4 22 invokestatic #29 <java/lang/String.valueOf> 25 invokespecial #33 <java/lang/StringBuilder.<init>> 28 aload_3 29 invokevirtual #36 <java/lang/StringBuilder.append> 32 aload_3 33 invokevirtual #36 <java/lang/StringBuilder.append> 36 aload_3 37 invokevirtual #36 <java/lang/StringBuilder.append> 40 invokevirtual #40 <java/lang/StringBuilder.toString> 43 astore 4
Podemos comprobar que aquí si ahorramos las llamadas a toString(), y por tanto es igual de eficiente que usar StringBuilder y se mejora la legibilidad.
Resumiendo, veamos cómo debemos manejar las cadenas:
- Usa el operador + si se concatena en una misma sentencia. Es más legible y el bytecode generado es el mismo.
- Usa StringBuilder en otro caso.
- En este caso no hemos tenido en cuenta si diversos hilos acceden a la misma cadena. En caso de que eso pudiera ocurrir, debes usar StringBuffer. Es algo más lento que StringBuilder, pero es thread safe
Espero que os haya gustado, y que Bastian se dé por satisfecho. Por mi parte, sólo queda agradecerle el hacerme más amenos los viajes en autobús.
Filed under: Coding, Tech | Tagged: bytecode, Java, JAVA Technology, performance, String, String concatenation | 5 Comments »