Manipulación de cadenas en Java

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

Test con StringBuilder:

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

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:

 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:

  1. Usa el operador + si se concatena en una misma sentencia. Es más legible y el bytecode generado es el mismo.
  2. Usa StringBuilder en otro caso.
  3. 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.

Follow

Get every new post delivered to your Inbox.

Join 27 other followers