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.
También te puede interesar
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 msLos 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.
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 separador | String.join(", ", lista) |
| Unir mientras transformas, con prefijo y sufijo | Collectors.joining(sep, prefijo, sufijo) |
| Ir añadiendo en un bucle con separador | StringJoiner |
| Construir un texto con formato libre en un bucle | StringBuilder |
¿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.