Anti-patrones en Java: concatenar cadenas en un bucle

3 min de lectura

Ir sumando trozos a una cadena con += dentro de un bucle es de esos errores que no se notan con diez elementos y que, con cien mil, dejan un proceso parado varios segundos. Lo medimos, y vemos de paso que la versión clásica tiene además un fallo con las listas vacías.

El código del problema

record Persona(String nombre) {}

String conMas(List<Persona> personas) {
    String s = "";
    for (Persona p : personas) s += ", " + p.nombre();
    return s.substring(2);
}

Funciona, y con tres nombres devuelve Ana, Luis, Marta. El problema es que las cadenas de Java no se pueden modificar: cada += crea una cadena nueva y copia en ella todo lo que ya había más el trozo nuevo. En la vuelta mil se copian los 999 nombres anteriores; en la diez mil, los 9,999. El trabajo total crece con el cuadrado del número de elementos.

Medido

Comparamos esa función con la versión de StringBuilder que hay más abajo, tomando la mejor de cinco ejecuciones para cada tamaño:

long medir(java.util.function.Supplier<String> f) {
    long mejor = Long.MAX_VALUE;
    for (int r = 0; r < 5; r++) {
        long t = System.nanoTime(); f.get(); mejor = Math.min(mejor, System.nanoTime() - t);
    }
    return mejor;
}
java Medir.javapersonas        +=   StringBuilder
     10,000       70 ms       1.18 ms
     20,000      260 ms       0.47 ms
     40,000     1001 ms       0.62 ms
     80,000     4338 ms       0.91 ms

Los milisegundos dependen de la máquina; lo que hay que mirar es cómo crecen. Cada vez que se dobla la lista, += tarda unas cuatro veces más: 70, 260, 1001, 4338. Es la firma de un algoritmo cuadrático. StringBuilder se queda en torno al milisegundo en todos los casos (a esas escalas el ruido de la medida pesa más que el trabajo, y por eso no crece de forma ordenada).

Y además falla con la lista vacía

El substring(2) del final quita la coma y el espacio que sobran al principio. Pero si la lista está vacía, no hay nada que quitar:

conMas(List.of())  —  salida recortadaException in thread "main" java.lang.StringIndexOutOfBoundsException: Range [2, 0) out of bounds for length 0
	...
	at Concatenar.conMas(Concatenar.java:9)

Es el típico arreglo que funciona en todas las pruebas hasta el día en que llega una lista vacía.

La versión con StringBuilder

String conBuilder(List<Persona> personas) {
    var sb = new StringBuilder();
    for (Persona p : personas) {
        if (sb.length() > 0) sb.append(", ");
        sb.append(p.nombre());
    }
    return sb.toString();
}

StringBuilder sí se puede modificar: va añadiendo al final de un búfer que crece cuando hace falta, así que cada nombre se copia una sola vez. Y la coma se pone antes de cada elemento salvo el primero, con lo que no hay nada que quitar al final y la lista vacía devuelve una cadena vacía.

Se le puede pasar una capacidad inicial, new StringBuilder(personas.size() * 16), para que no tenga que crecer por el camino. Es una mejora pequeña: el crecimiento ya es eficiente, y la diferencia de verdad está entre += y StringBuilder, no entre poner o no la capacidad.

Sin escribir el bucle

Para el caso más común, unir elementos con un separador, ni siquiera hace falta el StringBuilder:

IO.println(String.join(", ", tres.stream().map(Persona::nombre).toList()));
IO.println(tres.stream().map(Persona::nombre).collect(Collectors.joining(", ", "[", "]")));
var sj = new StringJoiner(" | ");
for (Persona p : tres) sj.add(p.nombre());
IO.println(sj);
salidaAna, Luis, Marta
[Ana, Luis, Marta]
Ana | Luis | Marta
Para…Se usa
Unir una lista de cadenas con un separadorString.join(", ", lista)
Unir mientras transformas, con prefijo y sufijoCollectors.joining(sep, prefijo, sufijo)
Ir añadiendo en un bucle con separadorStringJoiner
Construir un texto con formato libre en un bucleStringBuilder

¿Y el + fuera de los bucles?

No hay que tenerle miedo. Una expresión como "Hola, " + nombre + ". Tienes " + n + " mensajes" se resuelve en una sola operación: desde Java 9 el compilador la traduce a una llamada especializada que calcula el tamaño final y copia cada parte una vez. Cambiarla por un StringBuilder escrito a mano solo la hace más difícil de leer. El problema es acumular en un bucle, no el operador.

Entrada publicada en 2011 y revisada en octubre de 2026. Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio; las salidas están copiadas de esa ejecución.

Deja un comentario