Importancia de como concatenar una String en Java

[notice]Desde que este post fue creado han pasado 8 años así que puede que hayan cambiado cosas.[/notice]

Seguimos con los post sobre optimizaciones que pueden parecer una tontería pero que en grandes cantidades suponen una gran diferencia de tiempo. Supongamos que tenemos que extraer cierta información de la base de datos y generar una tabla que tengamos que enviar a alguien. Supongamos que la respuesta de la base de datos nos da unos 10000 registros y cada registro tiene unos 12 campos.

 

Usando concatenación ( variable += campo )

Lo primero que nos viene a la cabeza es usar algo como

String htmlCode = "";
htmlCode+= campo1Usuario1 + campo2Usuario1 + campo3Usuario1 + ... + campoNUsuario1;
htmlCode+= campo1Usuario2 + campo2Usuario2 + campo3Usuario2 + ... + campoNUsuario2;

System.out.println("Pintando contenido: "+htmlCode);

si tenemos pocos datos, veremos que tarda poco y si tenemos muchos datos, veremos que tarda bastante. Podriamos considerar que esto es normal debido a la gran cantidad de datos. Estamos hablando de 12 campos por 10000 registros, por lo que concatenar toda esa información lleva un tiempo.

¿Pero se puede mejorar? Si, y notaremos mucha mejora en grandes cantidades de datos. ¿Como?

 

StringBuffer

Que pasaría si usáramos la clase llamada StringBuffer? El código pasaría a verse de esta forma.

StringBuffer htmlCode = new StringBuffer("");
htmlCode.append(campo1Usuario1 + campo2Usuario1 + campo3Usuario1 + ... + campoNUsuario1);
htmlCode.append(campo1Usuario2 + campo2Usuario2 + campo3Usuario2 + ... + campoNUsuario2);

System.out.println("Pintando contenido: "+htmlCode);

Para tiempos pequeños podríamos notar muy poca diferencia, quizá ninguna y nos complica un poco mas la forma de picar el código, pero veremos que para grandes cantidades el tiempo que tarda comparado con la concatenación normal es muy grande.

Y ya esta? Pues no del todo, podemos ir un poquito mas lejos y usar otra clase

 

StringBuilder

La clase StringBuilder la usaremos igual que la clase StringBuffer, y veremos que tarda mas o menos el mismo tiempo, pero hay una pequeña diferencia. StringBuffer es synchronized internamente, esto nos asegura que si hacemos un append desde diferentes Threads a esa misma clase, StringBuffer se asegura de que hasta que no se haya terminado de concatenar los datos que han sido enviados desde un Thread, no se comenzará a concatenar el de otro Thread. En otras palabras, nos asegura exclusión mutua, evitando que se mezclen las letras de dos frases.

Esto podemos ahorrárnoslo siempre y cuando sepamos que esa clase va a ser ejecutada y llamada desde un único Thread.

StringBuilder htmlCode = new StringBuilder("");
htmlCode.append(campo1Usuario1 + campo2Usuario1 + campo3Usuario1 + ... + campoNUsuario1);
htmlCode.append(campo1Usuario2 + campo2Usuario2 + campo3Usuario2 + ... + campoNUsuario2);

System.out.println("Pintando contenido: "+htmlCode);

 

Pruebas de rendimiento

Aunque nos cuenten algo debemos intentar probarlo por nosotros mismos para realmente poder ver que es así. Aquí os dejo un código que realiza la tarea y las llamadas, devolviendo el tiempo que ha tardado en ejecutarse cada uno de los metodos.

 

public class StringTests {
	
	
	public static void main(String[] args) {
		
		StringBuilder phrase = new StringBuilder("<tr><td>Usuario nombre: Tal</td><td>Usuario password:</td>");
		phrase.append("<td>Usuario direccion: Tal</td><td>Usuario telefono:</td>");
		phrase.append("<td>Usuario telefono2: Tal</td><td>Usuario fnacimiento:</td>");
		phrase.append("<td>Usuario opcional1: Tal</td><td>Usuario opcional2:</td>");
		phrase.append("<td>Usuario opcional3: Tal</td><td>Usuario opcional4:</td>");
		phrase.append("<td>Usuario opcional5: Tal</td><td>Usuario opcional6:</td></tr>");

	
		System.out.print("Starting Concatenation ... ");
		System.out.println("Time Concatenation (ms): "+StringTests.generateStringUsingConcatenation(phrase.toString(), 10000));
		System.out.print("Starting StringBuffer ... ");
		System.out.println("Time StringBuffer (ms): "+StringTests.generateStringUsingStringBuffer(phrase.toString(), 10000));
		System.out.print("Starting StringBuilder ... ");
		System.out.println("Time StringBuilder (ms): "+StringTests.generateStringUsingStringBuilder(phrase.toString(), 10000));

	}

	public static long generateStringUsingConcatenation(String test, int iterations) {
		long initTime = System.currentTimeMillis(); 
		long stopTime = 0;

		String temporal = "";
		for (int i=0;i<iterations;i++) {
			temporal+=test;
		}

		stopTime = System.currentTimeMillis();
		return (stopTime - initTime);
		
	}

	public static long generateStringUsingStringBuffer(String test, int iterations) {
		long initTime = System.currentTimeMillis(); 
		long stopTime = 0;

		StringBuffer temporal = new StringBuffer("");
		for (int i=0;i<iterations;i++) {
			temporal.append(test);
		}

		stopTime = System.currentTimeMillis();
		return (stopTime - initTime);
	}
	
	public static long generateStringUsingStringBuilder(String test, int iterations) {
		long initTime = System.currentTimeMillis(); 
		long stopTime = 0;

		StringBuilder temporal = new StringBuilder("");
		for (int i=0;i<iterations;i++) {
			temporal.append(test);
		}

		stopTime = System.currentTimeMillis();
		return (stopTime - initTime);
	}	
	
}

 

Os dejo un vídeo demostrativo donde se puede ver lo que indico.

 

 

¿Donde realmente vamos a notar esta mejoría? Pues un dispositivo que disponga de poco hardware agradecerá que aprovechemos estas pequeñas cosillas. Esto puede ser el ejemplo para cuando tengamos que desarrollar una aplicación para un teléfono Android, donde no van muy sobrados de hardware en comparación con los ordenadores actuales. Supone una pequeña diferencia que hará que nuestras aplicaciones tengan un tiempo de respuesta muy rápido aún realizando grandes operaciones.

¿Pero porque pasa esto? ¿Que diferencia hay realmente? La respuesta esta en como Java concatena las Strings usando el concatenador += . Las Strings en Java no se pueden modificar, siempre que modificamos una, lo que estamos haciendo es crear una nueva String con los datos copiados y lo que queriamos cambiar, cambiado.

Por lo tanto cuando concatenamos

 

String texto = "stringA";
texto+= "stringB";
texto+= "stringC";

 

Lo que realmente esta pasando es que copiamos en una nueva String texto, la stringA y la stringB, Si hacemos lo mismo con stringC, entonces se volverá a crear una nueva String texto, se volverá a copiar stringA+stringB y luego stringC. Esto hace que para cada nueva concatenación volvamos a copiar lo que ya teníamos y lo nuevo en una nueva String.

Además hay un problema, y es que en el momento que se realiza esta copia, tenemos la String anterior todavía en memoria, por lo que estamos ocupando mas del doble de memoria para copiar una String, esto en dispositivos con poca memoria ram puede ser motivo de que el sistema operativo de la aplicación (si la String es muy grande) se quede sin memoria y mate a la aplicación.

Pudiendo encontrarnos con el siguiente mensaje, y a ver como encontramos el error…

android_appcrash

 

En el caso de StringBuffer o StringBuilder, el comportamiento sería mas bien como una lista dinámica de caracteres típica de C, donde lo único que hacemos es añadir un nuevo carácter, haciendo que realmente no copiemos lo que ya existía en la String, antes de que concatenáramos.

En este post de StackOverflow se puede ver la explicación: http://stackoverflow.com/questions/2721998/how-java-do-the-string-concatenation-using

En teoría si el compilador tiene habilitadas optimizaciones debería encargarse de transformar estas concatenaciones a StringBuilder para acelerar el proceso, pero dependiendo de lo bueno que sea el compilador o si no hemos activado esas optimizaciones, no se convertirá.

Lo mejor que podemos hacer, es poner nosotros el código correctamente para evitar dejar al compilador tomar estas decisiones.

 

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.