Análisis de rendimiento al concatenar Strings

Miguel Rodriguez
Pragma
Published in
7 min readJul 10, 2024

A medida que nuestras aplicaciones en Java evolucionan y se expanden en funcionalidades, la eficiencia en el uso de recursos se convierte en un aspecto crítico para asegurar el éxito y la durabilidad del proyecto. La atención dedicada a estos aspectos toma un rol fundamental en el desarrollo de software.

En Pragma nos esforzamos por siempre desarrollar código de calidad que sea legible y mantenible; es por eso que en esta oportunidad y en pro de #SaberMasParaResolverMejor te compartimos este artículo que tiene el objetivo de enseñarte herramientas para mejorar la gestión de memoria y el rendimiento.

Definición y alcance del problema

No solo se trata de escribir código que funcione; se trata de garantizar su óptimo desempeño. La correcta asignación y liberación de recursos, junto con la optimización, no solo mejora la eficiencia operativa de nuestras aplicaciones, sino que también contribuye a su escalabilidad y mantenibilidad a lo largo del tiempo.

Un enfoque eficaz en estos aspectos asegura que nuestras aplicaciones no solo cumplan con los requisitos actuales, sino que también estén preparadas para crecer y adaptarse a las demandas futuras.

El propósito fundamental de este artículo es mostrar que en ciertos casos específicos podemos mejorar el rendimiento de nuestras aplicaciones usando las clases e implementaciones adecuadas para cada necesidad en Java.

Contenido

Veamos juntos cómo se comparan las distintas formas de concatenación de Strings y sus ventajas y desventajas para garantizar no solo el funcionamiento del código, sino además, que se esté usando la memoria de la mejor forma posible:

Caso 1: Uso de StringBuilder

Son muy frecuentes los casos en el que en algún momento en nuestro desarrollo de alguna aplicación, nos es necesario realizar la concatenación de cadenas. Está resulta ser una operación básica, pero es de vital importancia saber de qué forma realizamos esta operación entendiendo como utiliza la memoria.

Ejemplo 1: Concatenando Strings con operador aritmético “+”

String name = "Miguel";
String lastName = "Rodriguez";
String fullname = name + " " + lastName;
System.out.println(fullname);

Ejemplo 2: Concatenando Strings con StringBuilder

String name = "Miguel";
String lastName = "Rodriguez";
StringBuilder fullname = new StringBuilder();
fullname.append(name).append(" ").append(lastName);
System.out.println(fullname.toString());

El uso de StringBuilder en lugar de la concatenación directa de cadenas (+=) es más eficiente porque StringBuilder crea y manipula un solo objeto mutable, evitando la creación innecesaria de objetos de cadena temporales.

Ahora veamos cómo puede afectar esto a nivel de tiempo de ejecución con la siguiente prueba:

Creamos 2 funciones que se encarguen de la concatenación de los Strings:

public static String concatString(String name, String lastName) {
return name + " " + lastName;
}

public static String concatStringBuilder(String name, String lastName) {
return new StringBuilder().append(name).append(" ").append(lastName).toString();
}

Ahora realizamos una prueba donde se llaman a estas funciones múltiples veces para ver la diferencia de tiempo de ejecución en milisegundos:

String name = "Miguel";
String lastName = "Rodriguez";

Long millisInitial, millisFinal = 0L;
Long millisBuilderInitial, millisBuilderFinal = 0L;

millisInitial = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
concatString(name, lastName);
}
millisFinal = System.currentTimeMillis();

millisBuilderInitial = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
concatStringBuilder(name, lastName);
}
millisBuilderFinal = System.currentTimeMillis();

System.out.println("Without Builder: " + (millisFinal - millisInitial));
System.out.println("With Builder: " + (millisBuilderFinal - millisBuilderInitial));

Dependiendo de la máquina que tengamos los resultados podrían ser más altos o bajos que los obtenidos en este ejemplo, lo que sí queda claro es que resulta más rápido realizar la concatenación con StringBuilder.

En resumen, para operaciones simples y esporádicas, el operador “+” es más adecuado por su simplicidad y legibilidad. La sintaxis concisa y directa hace que el código sea fácil de entender y mantener, especialmente cuando se trata de concatenar un pequeño número de cadenas. Por otro lado, para manipulaciones más complejas y repetitivas, especialmente dentro de bucles, StringBuilder es la opción preferida debido a su eficiencia y mejor rendimiento. StringBuilder es más adecuado para escenarios que involucran numerosas operaciones de concatenación o manipulación de cadenas porque evita la creación de múltiples objetos intermedios y reduce la sobrecarga de memoria y procesamiento.

Caso 2: Uso de String Template

Esta función nos permite crear cadenas de forma más legible y concisa así como se aprecia en el siguiente ejemplo de un reporte de ventas:

String product1 = "Milk", product2 = "Rice";
Integer soldUnits1 = 3, soldUnits2 = 2;
Double price1 = 400.0, price2 = 300.0;

String report = """
**Sales Report**
%s: %d units - Total price: $%.2f
%s: %d units - Total price: $%.2f
...
""".formatted(product1, soldUnits1, soldUnits1 * price1,
product2, soldUnits2, soldUnits2 * price2);
System.out.println(report);

Pero esto no quiere decir que sea eficiente a nivel de procesamiento, y para comprobarlo compararemos su rendimiento contra el String Builder con las siguientes dos funciones:

public static String concatStringTemplate(String name, String lastName){
return "%s %s".formatted(name, lastName);
}

public static String concatStringBuilder(String name, String lastName) {
return new StringBuilder().append(name).append(" ").append(lastName).toString();
}

Ahora realizamos una prueba donde se llaman a estas funciones múltiples veces para ver la diferencia de tiempo de ejecución en milisegundos:

String name = "Miguel";
String lastName = "Rodriguez";

Long millisInitial, millisFinal = 0L;
Long millisBuilderInitial, millisBuilderFinal = 0L;

millisInitial = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++){
concatStringTemplate(name, lastName);
}
millisFinal = System.currentTimeMillis();

millisBuilderInitial = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++){
concatStringBuilder(name, lastName);
}
millisBuilderFinal = System.currentTimeMillis();

System.out.println("With Template: " + (millisFinal - millisInitial));
System.out.println("With Builder: " + (millisBuilderFinal - millisBuilderInitial));

Como se había dicho anteriormente, el String Template no ofrece el mejor rendimiento y su uso dependerá de sus necesidades específicas. Si necesita concatenar cadenas de caracteres de manera eficiente, use StringBuilder. Si necesita hacerlo de forma más legible y concisa, use String Template.

La plantilla de cadenas String Template, es ideal para mejorar la organización y legibilidad del código en la generación de reportes y formatos complejos. Por ejemplo, en sistemas empresariales para informes financieros o en aplicaciones de gestión de inventarios para reportes detallados donde se tengan que generar documentos digitales con cierta estructura. Esta forma de manejar Strings asegura una presentación coherente y profesional de los datos, facilitando el mantenimiento y la comprensión del código.

Caso 3: Uso de String Buffer

En este caso vamos a comparar el rendimiento entre String Buffer y String Builder y ver cual es más eficiente, además de entender en qué momento podemos usar uno o el otro dependiendo de las necesidades.

Primero realizamos la implementación de ambas herramientas para compararlas.

Ejemplo 1: Concatenando Strings con StringBuilder vista anteriormente

String name = "Miguel";
String lastName = "Rodriguez";

StringBuilder fullname = new StringBuilder();
fullname.append(name).append(" ").append(lastName);

System.out.println(fullname.toString());

Ejemplo 2: Concatenando Strings con StringBuffer

String name = "Miguel";
String lastName = "Rodriguez";

StringBuffer fullname = new StringBuffer();
fullname.append(name).append(" ").append(lastName);

System.out.println(fullname.toString());

Como se observa en ambos fragmentos de código, que corresponden a los métodos append del StringBuffer y el StringBuilder respectivamente, el uso de ambas herramientas es muy similar, pero poseen una diferencia vital que hace que difieran en rendimiento, y es que el String Buffer en su implementación hace uso de Synchronized para asegurarse de que solo un hilo pueda editar una instancia en un mismo instante de tiempo.

Función append de StringBuffer

@Override
@IntrinsicCandidate
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}

Función append de StringBuilder

@Override
@IntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this;
}

Realicemos una prueba para ver que tanta diferencia hay entre ambas herramientas. Primero creamos dos métodos que se encarguen de realizar concatenación de cadenas por ambos métodos

public static String concatStringBuilder(String name, String lastName) {
return new StringBuilder().append(name).append(" ").append(lastName).toString();
}

public static String concatStringBuffer(String name, String lastName) {
return new StringBuffer().append(name).append(" ").append(lastName).toString();
}

Ahora realizaremos una prueba de llamados a estos métodos y veremos sus resultados finales

String name = "Miguel";
String lastName = "Rodriguez";

Long millisBufferInitial, millisBufferFinal = 0L;
Long millisBuilderInitial, millisBuilderFinal = 0L;

millisBufferInitial = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++){
concatStringBuffer(name, lastName);
}
millisBufferFinal = System.currentTimeMillis();

millisBuilderInitial = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++){
concatStringBuilder(name, lastName);
}
millisBuilderFinal = System.currentTimeMillis();

System.out.println("With Buffer: " + (millisBufferFinal - millisBufferInitial));
System.out.println("With Builder: " + (millisBuilderFinal - millisBuilderInitial));

Como resultado final vemos que el String Builder más eficiente que el String Buffer aunque la diferencia no es muy grande en este caso, y dado que cada desarrollo es diferente tenemos que validar que herramienta se adapta mejor a nuestras necesidades. Si el requerimiento incluye asegurar la consistencia de nuestro String aunque sea editado por múltiples hilos, podemos usar el Buffer para lograrlo.

El StringBuffer es esencial para manipular cadenas en entornos multihilo donde se requiere concatenar texto de forma concurrente sin riesgos. Por ejemplo, en sistemas de registro de logs en tiempo real, donde se deben manejar grandes volúmenes de datos de manera continua y eficiente. Además, en aplicaciones que generan informes dinámicos basados en datos en tiempo real, el StringBuffer facilita la construcción y formateo dinámico de cadenas de texto de manera eficaz, garantizando un rendimiento óptimo y sin conflictos.

Ten en cuenta que estas herramientas son usadas en casos muy específicos y su aplicación dependerá del contexto o del problema que estemos tratando de solucionar

Conclusión

Hagamos un repaso de lo que hicimos:

  • Aprendimos que hasta una acción tan simple como una concatenación puede implicar en menor o mayor medida el rendimiento de nuestros desarrollos.
  • Aprendimos el uso de algunas herramientas en diferentes circunstancias y las razones que hacen que haya diferencias en su rendimiento individual.
  • Aprendimos que el StringBuilder es mejor para manipulaciones más complejas y repetitivas de cadenas, especialmente dentro de bucles; el String Template es ideal para mejorar la organización y legibilidad del código en la generación de reportes y formatos complejos; y el StringBuffer es esencial para manipular cadenas en entornos multihilo donde se requiere concatenar texto de forma concurrente sin riesgos.

--

--

Miguel Rodriguez
Pragma
0 Followers
Writer for

Soy un ingeniero de sistemas y computación, con un enfoque en el área de desarrollo backend.