Java básico: String (I)

Lunes, 24 de Octubre de 2005

Cuando aprendemos Java, vamos aprendiendo aspectos del lenguaje muy peculiares. Uno de ellos es que “todo es un objeto”, incluso las cadenas de texto que son del tipo java.lang.String. Después aprendemos que Java no sabe concatenar cadenas y que el operador “+” es el único que está “sobrecargado”. Es decir, el operador “+” en Java equivale a crear un nuevo objeto String distinto cuyo contenido es la suma de los dos objetos String que acabamos de concatenar.
Y por esta razón a veces vemos código como el siguiente:

long result = System.currentTimeMillis();
System.out.println("El resultado final es "+result+ " milisegundos");

E, ingenuos de nosotros, nos entra una desagradable sensación de culpabilidad, por lo que nos apiadamos de Java y decidimos ponérselo más fácil, sustituyéndo el código tan costoso por esta versión supuestamente optimizada:

long result = System.currentTimeMillis();
StringBuffer sb = new StringBuffer();
sb.append("El resultado final es ");
sb.append(result);
sb.append(" milisegundos");
System.out.println(sb.toString());

Pues no ha servido para nada. Los dós códigos son equivalentes (que no iguales). Y es que cómo Java no sabe sumar cadenas, cuando se encuentra un código como el primero, el compilador lo sustituye raudo y veloz por un código como el siguiente:

long result = System.currentTimeMillis();
System.out.println(
    new StringBuffer().append("El resultado final es ")
    .append(result).append(" milisegundos").toString());

Por lo que nuestra optimización inicial, aunque bien intencionada, no ha servido para nada. Java ya sabe hacer eso por nosotros. Caer en la tentación del principiante de intentar optimizar el código de nuestra aplicación, sustituyendo cada concatenación que vemos por la utilización de StringBuffers es un esfuerzo innecesario que no debemos malgastar.

Es más, si por un casual intentamos concatenar constantes:

System.out.println("Pulsa cualquier teclan"+
                   "para continuarn");

Java lo detecta y las concatena en tiempo de compilación, sin generar código redundante:

System.out.println("Pulsa cualquier teclanpara continuarn");

Pero vayamos a otro caso distinto. Supongamos este codigo:

String numeros = "";
for (int n=1;n<100;n++) {
    numeros = numeros + n + " ";
}
System.out.println(numeros);

?Qué está pasando aquí? Bueno, pues si sabemos que Java no sabe concatenar y que, por cada concatenación va a construir un objeto String nuevo, podemos llegar a la conclusión de que así, a ojo, va a crear 100 objetos String intermedios para solo quedarse con uno, el último de ellos.
Tomando como ejemplo el caso anterior, veamos lo que haría el compilador:

String numeros = "";
for (int n=1;n<100;n++) {
    numeros = new StringBuffer().append(numeros).append(n).append(" ").toString();
}
System.out.println(numeros);

Está claro que no hemos ganado nada. Se siguen creando y destruyendo 100 objetos String y StringBuffer, y solo nos quedamos con la última instancia del String resultante. Nada óptimo, sin lugar a dudas (el consumo másivo de objetos en memoria sin justificación es el mayor enemigo del rendimiento de nuestra aplicación). Así que la solución es fácil: ya que sabemos usar StringBuffer, aprovechemos lo que sabemos para ahorrarle trabajo a Java. El código ideal sería así:

StringBuffer numeros = new StringBuffer();
for (int n=1;n<100;n++) {
    numeros.append(n).append(" ");
}
System.out.println(numeros.toString());

Ahora si que le hemos ahorrado a Java trabajo, pero le hemos ahorrado el trabajo de la generación innecesaria de objetos, no la de concatenar.
Conclusión:

  • Java si sabe concatenar: Cada vez que aparece una concatenación, crea un objeto StringBuffer y sustituye el operador “+” tantas veces cómo sea necesario por llamadas a métodos append() de la clase StringBuffer. Esta sustitución es atómica y solo se realiza una vez por cada línea, por lo que cada vez que queramos concatenar cadenas, intentaremos siempre hacerlo en la misma línea.
  • Java no sabe ahorrar: Si utilizamos la concatenación dentro de un bucle, Java realizará la sustitución anterior en el interior del mismo, con el consiguiente desperdicio de memoria al crear tantas instancias de String y StringBuffer como repeticiones tenga el bucle. Solo en este caso, si procede utilizar StringBuffer.

Os invito a que probeis diferentes versiones de concatenación de Strings, las compileis y después las decompileis. Un buen descompilador que utilizo con frecuencia es Jad. Veréis como Java es más listo de lo que parece… en algunas ocasiones.

7 comentarios to “Java básico: String (I)”

  1. Alf:

    Mmm… ?estás seguro de que el compilador es suficientemente inteligente para eso? Tengo entendido que cuando concatenas varios Strings con el operador “+”, la máquina virtual crea una cadena nueva por cada “par”. Es decir en

    String algo = "esto " + "es " + "una tontería";

    la VM crea primero un String “esto es ” y luego crea el String definitivo “esto es una tontería”.

    Al menos eso pone en muchos libros. ?Tal vez, es alguna mejora de las últimas versiones?

  2. Anonymous:

    Lo que dices es cierto. Si concatenas constantes, el compilador creara una constante nueva con la suma de las anteriores. Es igual que el ejemplo que pongo de:

    Es más, si por un casual intentamos concatenar constantes:

    System.out.println(”Pulsa cualquier tecla\n”+
    “para continuar\n”);

    Java lo detecta y las concatena en tiempo de compilación, sin generar código redundante:

    System.out.println(”Pulsa cualquier tecla\npara continuar\n”);

    Pero si no son constantes, utilizará StringBuffer. Te invito a que lo compruebes con un decompilador. Veras como Java te “toquetea” el codigo. :-)

  3. Alf:

    He probado algunos ejemplos con el Jad (incluídos los que pones) y no utiliza StringBuffers. Sólo optimiza cuando concateno constantes.

    Yo utilizo la versión 1.4. ?Tal vez es una mejora de la 1.5? Eso sí, el Jad me dice que la versión del fichero class es la 48.0, y que no la soporta.

  4. Anonymous:

    Con que lo compilas? Algun IDE en concreto (IDEA, JBuilder, Eclipse) o directamente desde la linea de comandos con javac?

  5. sole:

    Ahora si que le hemos ahorrado a Java trabajo, pero le hemos ahorrado el trabajo de la generación innecesaria de objetos, no la de concatenar.
    Conclusión:

    * Java si sabe concatenar: Cada vez que aparece una concatenación, crea un objeto StringBuffer y sustituye el operador “+” tantas veces cómo sea necesario por llamadas a métodos append() de la clase StringBuffer. Esta sustitución es atómica y solo se realiza una vez por cada línea, por lo que cada vez que queramos concatenar cadenas, intentaremos siempre hacerlo en la misma línea.
    * Java no sabe ahorrar: Si utilizamos la concatenación dentro de un bucle, Java realizará la sustitución anterior en el interior del mismo, con el consiguiente desperdicio de memoria al crear tantas instancias de String y StringBuffer como repeticiones tenga el bucle. Solo en este caso, si procede utilizar StringBuffer.

    Os invito a que probeis diferentes versiones de concatenación de Strings, las compileis y después las decompileis. Un buen descompilador que utilizo con frecuencia es Jad. Veréis como Java es más listo de lo que parece… en algunas ocasiones.

  6. Anonymous:

    Agradezco tu comentario Sole, pero no he tenido mas remedio que modificar tu texto para quitar los casi 90 enlaces que has metido a una unica paginas personal (no se si tuya) por ser considerado como spam. Un saludo.

  7. Using correctly String objects in Java (Uso óptimo de String en Java) « JNOJ3681 Blog:

    […] http://albertovilches.com/java_basico_string_i (in spanish) […]

Hacer un comentario

XHTML: Puedes utilizar las siguientes etiquetas:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>

Verification Image

Debes leer y teclear los caracteres entre 0..9 y A..F para enviar la respuesta.