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.

About these ads

5 Responses

  1. Hace mucho tiempo que no me peleo con Java, pero creo recordar que los String en java son inmutables. No deberias tener en cuenta tambien el efecto que tiene sobre el stack?

    String s=”hola”
    for(int i=1;i<10000;i++)
    s+= i;

    Eso deberia crear 10000 nuevos literales (constantes) que quedarian en memoria y que se reutilizarian.

    Cual seria entonces el efecto de algo asi.

    String s=”hola”
    for(int i=1;i<10000;i++)
    s= “hola”+”a”;

    Solo esta haciendo la concatenacion la primera vez que pasa por el bucle, el resto seria equivante a

    s= “holaa”;

    Claro que es un caso completamente irreal . Que concatena 10000 veces la misma cadena :P

    Lo dicho que hablo de memoria, asi que no me hagas mucho caso.

  2. Me doy, me doy.
    Parece que el compilador no es tan inteligente.

    Gracias por el curro, yo estaba un poco vaguete como para probarlo. ;)

  3. Manipulación de cadenas en Java

    Análisis de las diferencias entre String y StringBuilder en Java.

  4. Muy interesante, con tanta abstracción, demasiadas veces se nos olvida a todos que hay unas normas “físicas” que cumplir y que el peaje que tenemos que pagar es en ocasiones “alto”.

  5. [...] Tienes todo esto mucho más detallado en Manipulación de cadenas en java. [...]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 27 other followers

%d bloggers like this: