UD2 · Sección 7 — StringBuilder y concatenación eficiente
| Dato | Valor |
|---|---|
| Unidad Didáctica | UD2 — Programación Orientada a Objetos con la API de Java |
| Resultado de Aprendizaje | RA2 — Escribe y prueba programas sencillos, reconociendo y aplicando los fundamentos de la programación orientada a objetos. |
| Sección | 7 de 10 — StringBuilder y concatenación eficiente |
| Evaluación | 1.ª Evaluación |
| Duración estimada | 2 horas lectivas |
| Criterios de evaluación cubiertos | CE 2.2 (escribe programas simples), CE 2.3 (instancia objetos a partir de clases predefinidas), CE 2.4 (utiliza métodos y propiedades), CE 2.6 (utiliza parámetros), CE 2.8 (utiliza constructores) |
| Prerrequisitos | UD1 completa, UD2 Secciones 1 a 6 |
| Paquete del código de ejemplo | dam.programacion.ud2 |
| Herramientas | JDK 25, IntelliJ IDEA Community, Maven 3.9.x |
1. Punto de partida
En la Sección 3 descubriste que String es inmutable: cualquier operación que parezca modificar un string (concatenar, sustituir, transformar) en realidad crea un String nuevo. Para operaciones puntuales eso no es problema. Pero ¿qué pasa si construyes un texto dentro de un bucle que se ejecuta mil veces? Cada iteración crea un String intermedio que se descarta en la siguiente, y al final tienes mil objetos en el heap esperando a que el garbage collector los limpie. Eso es ineficiente en memoria y en tiempo.
Para esos casos, Java ofrece StringBuilder: una clase mutable diseñada específicamente para construir y modificar cadenas de forma eficiente. En esta sección verás por qué StringBuilder es mejor que la concatenación con + en bucles, cómo se usa, y qué alternativas más modernas existen (StringJoiner y String.join, que ya conoces parcialmente de la Sección 3).
Al terminar la sección serás capaz de construir textos largos de forma eficiente, elegir entre String, StringBuilder y StringJoiner según el caso, y entender por qué la concatenación con + no siempre es mala (de hecho, el compilador la optimiza en muchos casos).
2. El problema de la concatenación con + en bucles
2.1. Recordatorio: inmutabilidad de String
Cada + genera un nuevo String. En una operación aislada no pasa nada: el coste es mínimo. Pero en un bucle, el coste se acumula.
2.2. El escenario problemático
Imagina que construyes un informe uniendo los nombres de 1000 clientes:
¿Qué pasa en cada iteración?
- Se crea un
Stringintermedio coninforme + nombre. - Se crea otro
Stringintermedio con el resultado anterior +", ". - Ese resultado se asigna a
informe. - El
informeanterior queda sin referencia y se marca para GC.
En total, para 1000 nombres, el programa crea 2000 strings (los intermedios y los finales) y los descarta. El coste en memoria es enorme y el tiempo de ejecución crece cuadráticamente (el primer nombre se copia 1000 veces, el segundo 999, el tercero 998...).
2.3. La solución: StringBuilder
StringBuilder es mutable: vas añadiendo texto a un mismo objeto sin crear nuevos. Al final, llamas a toString() para obtener el String resultante.
Aquí solo se crea un objeto StringBuilder y un String final. Los fragmentos se añaden al buffer interno del StringBuilder, que se redimensiona automáticamente cuando hace falta.
Analogía del cuaderno. Imagina que quieres copiar 1000 frases en un cuaderno.
- Con
+(concatenación): cada frase que copias requiere pasar a una hoja nueva. Al final tienes 1000 hojas usadas (losStringintermedios) y has copiado la primera frase 1000 veces, la segunda 999, etc. Un desastre. - Con
StringBuilder: escribes todas las frases seguidas en el mismo cuaderno. Cuando una hoja se llena, pasas a la siguiente (redimensión del buffer). Al final tienes un cuaderno con todas las frases, en orden, sin desperdiciar.
3. La clase StringBuilder
3.1. Constructores
| Constructor | Qué hace |
|---|---|
StringBuilder() |
Crea un StringBuilder vacío (capacidad inicial por defecto: 16 caracteres) |
StringBuilder(int capacidad) |
Crea un StringBuilder vacío con la capacidad inicial indicada |
StringBuilder(String s) |
Crea un StringBuilder con el contenido inicial de s |
StringBuilder(CharSequence cs) |
Crea un StringBuilder con el contenido de cualquier CharSequence |
Capacidad. Internamente, StringBuilder mantiene un array de caracteres. Si lo llenas, Java crea un array nuevo más grande y copia el contenido. Para evitar redimensionamientos innecesarios (que son costosos), conviene crear el StringBuilder con una capacidad inicial cercana al tamaño final esperado.
3.2. Métodos principales
| Método | Qué hace |
|---|---|
append(tipo) |
Añade al final (admite String, int, double, boolean, char, Object, etc.) |
insert(int offset, tipo) |
Inserta en la posición indicada |
delete(int start, int end) |
Elimina los caracteres entre start (inclusivo) y end (exclusivo) |
deleteCharAt(int index) |
Elimina un carácter concreto |
replace(int start, int end, String s) |
Sustituye el rango por s |
reverse() |
Invierte el contenido |
setCharAt(int index, char c) |
Cambia un carácter concreto |
substring(int start) / substring(int start, int end) |
Devuelve un String (sin modificar el StringBuilder) |
charAt(int index) |
Devuelve el carácter en la posición |
length() |
Longitud actual |
capacity() |
Capacidad actual del buffer |
toString() |
Devuelve el String resultante |
indexOf(String) / lastIndexOf(String) |
Búsqueda (como en String) |
Ejemplos:
Observa que los métodos modifican el StringBuilder (es mutable) y devuelven una referencia al propio StringBuilder, lo que permite encadenar llamadas:
3.3. append admite muchos tipos
A diferencia de +, que solo funciona entre String y otros tipos mediante conversión automática, append está sobrecargado para todos los tipos primitivos y Object:
Esto es más eficiente que concatenar con + en un bucle, porque cada append solo añade al buffer interno sin crear String intermedios.
4. Cuándo usar + y cuándo StringBuilder
Una idea muy extendida entre principiantes es que "concatenar con + es siempre malo". Es falso. El compilador de Java optimiza la concatenación con + en muchos casos, convirtiéndola internamente en StringBuilder automáticamente.
4.1. Caso optimizado: concatenación en una sola línea
El compilador transforma esta línea en algo equivalente a:
Es decir, una sola línea con varios + es tan eficiente como un StringBuilder manual. No tienes que preocuparte por optimizarla.
4.2. Caso NO optimizado: concatenación en un bucle
El compilador traduce cada informe = informe + nombre + ", " como un StringBuilder nuevo, lo que da lugar a 1000 StringBuilder y 1000 String intermedios. Ahí sí conviene reescribir:
4.3. Reglas prácticas
| Situación | Recomendación |
|---|---|
| Concatenar 2-3 strings en una sola línea | + (lo optimiza el compilador) |
Construir un mensaje con + en una sola expresión |
+ o String.format |
| Construir un texto en un bucle | StringBuilder |
| Unir elementos de una colección con separador | String.join o StringJoiner |
| Construir un texto condicional con muchas ramas | StringBuilder |
| Construir SQL, JSON, HTML manualmente | StringBuilder (o mejor, una librería específica) |
Analogía de los cubiertos. Imagina que + es un tenedor y StringBuilder es un cuchillo de cocina.
- Para comer un trozo de carne (operación simple), el tenedor basta.
- Para filetear una pieza entera (muchas operaciones encadenadas), necesitas el cuchillo.
No uses el cuchillo para comer un trozo de carne; no uses el tenedor para filetear.
5. StringJoiner: unir con separador, prefijo y sufijo
Java 8 introdujo StringJoiner, una clase pensada para unir varios elementos con un separador, opcionalmente con un prefijo y un sufijo. Es especialmente útil cuando quieres producir un texto con un formato fijo, como una lista entre corchetes.
5.1. Constructor y métodos básicos
| Constructor | Qué hace |
|---|---|
StringJoiner(CharSequence separador) |
Crea un StringJoiner con el separador indicado, sin prefijo ni sufijo |
StringJoiner(CharSequence separador, CharSequence prefijo, CharSequence sufijo) |
Igual, con prefijo y sufijo |
Métodos principales:
| Método | Qué hace |
|---|---|
add(CharSequence) |
Añade un elemento |
merge(StringJoiner otro) |
Fusiona otro StringJoiner |
setEmptyValue(CharSequence) |
Valor a devolver si está vacío |
toString() |
Devuelve el String resultante |
length() |
Longitud total |
5.2. Ejemplo
StringJoiner se usa internamente por el método String.join (Sección 3) y por el Collectors.joining de la Streams API (UD6). Rara vez lo usarás directamente, pero conviene saber que existe.
6. String.join: la alternativa concisa
Ya lo vimos en la Sección 3, lo recordamos aquí porque encaja temáticamente. Es un método estático que usa StringJoiner internamente y permite unir elementos con un separador en una sola línea.
Cuándo usar String.join vs StringJoiner:
- Si ya tienes los elementos en un array o varargs, usa
String.join. - Si necesitas prefijo/sufijo o vas añadiendo elementos dinámicamente, usa
StringJoiner.
7. StringBuffer: el primo thread-safe
Existe una tercera clase similar: StringBuffer. Es idéntica a StringBuilder en API, pero es sincronizada (thread-safe). Esto significa que varios hilos pueden usar el mismo StringBuffer simultáneamente sin corromper su estado.
| Clase | Mutable | Thread-safe | Rendimiento | Cuándo usar |
|---|---|---|---|---|
String |
No | Sí (por inmutabilidad) | Bajo en concatenaciones repetidas | Para textos fijos |
StringBuilder |
Sí | No | Alto | Caso general |
StringBuffer |
Sí | Sí | Medio (sincronización) | Multihilo (raro en código moderno) |
Recomendación práctica: usa StringBuilder siempre. Si en algún momento necesitas compartir el buffer entre hilos (avanzado, se verá más adelante en el curso), entonces reconsidera StringBuffer. Pero hoy en día, en la mayoría de casos multihilo se prefieren otras aproximaciones (variables locales, inmutabilidad, etc.).
8. Diagrama comparativo
graph TD
A["Necesito construir/modificar un texto"]
A --> B{"Es en un bucle o<br/>muchas operaciones?"}
B -->|No, una sola linea| C["Usa + o String.format"]
B -->|Si| D{"Quiero unir elementos<br/>con separador?"}
D -->|Si, en una linea| E["String.join"]
D -->|Si, con prefijo/sufijo<br/>o elementos dinamicos| F["StringJoiner"]
D -->|No, es construccion<br/>paso a paso| G["StringBuilder"]
B -->|Si, y necesito<br/>thread-safety| H["StringBuffer"]
C --> R["String final"]
E --> R
F --> R
G --> R
H --> R
style C fill:#2e7d32,color:#fff,stroke:#1b5e20
style E fill:#2e7d32,color:#fff,stroke:#1b5e20
style F fill:#ef6c00,color:#fff,stroke:#e65100
style G fill:#1565c0,color:#fff,stroke:#0d47a1
style H fill:#6a1b9a,color:#fff,stroke:#4a148c
9. Ejemplo integrador
Programa que demuestra los conceptos de la sección: construcción de un informe con + (ineficiente), con StringBuilder (eficiente), con StringJoiner y con String.join.
Salida esperada:
10. Errores comunes
| Error | Qué pasa | Cómo evitarlo |
|---|---|---|
Concatenar con + en un bucle |
Ineficiente: crea muchos String intermedios |
Usa StringBuilder con append |
Olvidar toString() al final |
Tienes un StringBuilder pero no un String |
Llama siempre a toString() para obtener el String final |
Crear StringBuilder sin capacidad inicial cuando conoces el tamaño aproximado |
Redimensionamientos internos | Pasa la capacidad: new StringBuilder(1000) |
Confundir replace(int, int, String) de StringBuilder con replace(char, char) de String |
Firmas distintas | Lee el Javadoc: en StringBuilder recibe posiciones y un String |
Usar StringBuilder para 2-3 concatenaciones en una línea |
Código más verboso sin beneficio | Usa + (el compilador lo optimiza) |
Usar StringBuffer por defecto |
Sobrecoste innecesario por sincronización | Usa StringBuilder salvo que necesites thread-safety |
Olvidar el import java.util.StringJoiner; |
"cannot find symbol" | Añade el import (o Alt+Enter en IntelliJ) |
Modificar un String en un bucle esperando que cambie el original |
El cambio se pierde | Recuerda: String es inmutable, hay que asignar |
Llamar a substring de StringBuilder pensando que modifica el objeto |
Devuelve un String nuevo, no afecta al StringBuilder |
Si quieres recortar, usa delete |
11. Resumen
| Concepto | Idea clave |
|---|---|
String inmutable |
Cualquier operación crea un String nuevo |
Concatenación con + en una sola línea |
El compilador la optimiza a StringBuilder |
Concatenación con + en bucle |
Ineficiente: crea muchos objetos intermedios |
StringBuilder |
Clase mutable para construir textos eficientemente |
StringBuilder.append(tipo) |
Añade al final (admite cualquier tipo) |
StringBuilder.insert/delete/replace/reverse |
Operaciones de modificación |
StringBuilder.toString() |
Obtiene el String final |
| Capacidad inicial | Especificarla reduce redimensionamientos |
| Encadenamiento | sb.append(a).append(b).append(c) |
StringJoiner |
Une elementos con separador, prefijo y sufijo |
String.join(separador, elementos) |
Atajo estático para unir con separador |
StringBuffer |
Igual que StringBuilder pero thread-safe (rara vez necesario) |
| Regla práctica | + para casos simples, StringBuilder para bucles, String.join para unir con separador |
12. Cuestionario de autoevaluación
Preguntas
-
¿Por qué concatenar con
+en un bucle es ineficiente? Explica qué ocurre en cada iteración. -
¿Es
StringBuildermutable o inmutable? ¿YString? -
Escribe un fragmento de código que construya la cadena
"Hola, Maria. Tienes 23 anos."usandoStringBuilderconappend. -
¿El compilador optimiza la concatenación con
+en una sola línea? Si es así, ¿a qué la convierte? -
Nombra tres métodos de
StringBuilderdistintos deappendytoString. -
¿Qué diferencia hay entre
StringBuilderyStringBuffer? -
Si vas a construir un texto de aproximadamente 5000 caracteres, ¿por qué conviene crear el
StringBuilderconnew StringBuilder(5000)? -
Escribe una línea de código que una los strings
"Maria","Carlos"y"Ana"con el separador", "usandoString.join. -
¿Qué método de
StringBuilderusarías para invertir el contenido? ¿Devuelve unStringo modifica el propioStringBuilder? -
¿Cómo crearías un
StringJoinerque produzca[A, B, C](con corchetes como prefijo/sufijo)? -
¿Qué pasa si llamas a
substring(2, 5)sobre unStringBuilder? ¿Se modifica el objeto? -
Para construir el mensaje
"Hola, " + nombre + ". Tienes " + edad + " anos."en una sola línea, ¿usarías+oStringBuilder? ¿Por qué? -
¿Por qué
appenddeStringBuilderaceptaint,double,booleany otros tipos, no soloString? -
Si en un bucle de 1000 iteraciones concatenas con
+, ¿cuántos objetosStringintermedios se crean aproximadamente? -
¿Cuál es el equivalente en
String.joindel siguiente código?
Soluciones
-
En cada iteración, la expresión
informe + nombrecrea un nuevoString. Ese nuevoStringse asigna ainforme, y elinformeanterior queda sin referencia, esperando al garbage collector. En 1000 iteraciones se crean ~1000Stringintermedios, y como cada uno copia el contenido de los anteriores, el coste total crece cuadráticamente. -
StringBuilderes mutable: los métodos modifican el propio objeto.Stringes inmutable: cualquier operación devuelve unStringnuevo. -
Una solución:
-
Sí. El compilador transforma una concatenación con
+en una sola línea en una secuencia de llamadas aStringBuilder.appendterminada contoString(). Por eso en esos casos no hay pérdida de rendimiento. -
Posibles:
insert(int, tipo),delete(int, int),deleteCharAt(int),replace(int, int, String),reverse(),setCharAt(int, char),substring(int),charAt(int),length(),capacity(). -
StringBufferes sincronizado (thread-safe): varios hilos pueden usarlo a la vez sin corromper su estado.StringBuilderno lo es.StringBuilderes más rápido;StringBufferse reserva para casos multihilo donde se comparte el mismo objeto entre hilos (raro en código moderno). -
Porque
StringBuilderinternamente mantiene un array de caracteres. Si lo llenas, Java crea un array nuevo más grande y copia el contenido. Indicar la capacidad inicial evita redimensionamientos intermedios, que son costosos. -
String.join(", ", "Maria", "Carlos", "Ana"); -
reverse(). Modifica el propioStringBuildery devuelve una referencia a sí mismo (para permitir encadenamiento). No devuelve unString. -
StringJoiner sj = new StringJoiner(", ", "[", "]");y luegosj.add("A").add("B").add("C");. -
No se modifica.
substringenStringBuilderdevuelve unStringnuevo con la porción indicada; elStringBuilderoriginal queda intacto. Si quisieras recortar elStringBuilder, tendrías que usardelete. -
Usaría
+. En una sola línea, el compilador optimiza la concatenación a unStringBuilderinterno, así que es igual de eficiente que escribirlo manualmente y queda más legible. -
Porque
appendestá sobrecargado con muchas versiones (append(int),append(double),append(boolean),append(char),append(Object), etc.). Cada versión convierte su argumento aString(usandoString.valueOf) y lo añade al buffer. Esto evita tener que llamar aString.valueOfmanualmente. -
Aproximadamente 1000 (uno por iteración, el
informeintermedio). En realidad el doble si contamos elStringque se descarta y el nuevo que se asigna, pero la idea es: cada iteración produce unStringnuevo y descarta el anterior. -
String.join(", ", "a", "b", "c");o, con un array,String.join(", ", new String[]{"a","b","c"});.
Evaluación del cuestionario
| Aciertos | Nivel |
|---|---|
| 14-15 | Dominas la sección. Adelante con la siguiente. |
| 10-13 | Bien, repasa los puntos fallados. |
| 6-9 | Repasa la sección, especialmente cuándo usar cada alternativa. |
| Menos de 6 | Vuelve a leer con calma y ejecuta el ejemplo integrador paso a paso en IntelliJ. |
13. Información ampliada y próximos pasos
- Javadoc oficial de
StringBuilder(Java 25): https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/StringBuilder.html - Javadoc oficial de
StringJoiner: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/StringJoiner.html - Artículo "String Concatenation in Java" (Baeldung): https://www.baeldung.com/java-string-concatenation
- Artículo "StringBuilder vs StringBuffer" (Baeldung): https://www.baeldung.com/java-string-builder-string-buffer
Próximos pasos:
- Practica en IntelliJ. Copia el ejemplo integrador y modifícalo: cambia los nombres, los separadores, prueba a construir un pequeño informe con varias líneas usando
\ncomo separador. - Compara rendimiento. Si te animas, escribe dos versiones de un programa que concatene 10.000 números en un
String: una con+en bucle y otra conStringBuilder. Mide el tiempo conSystem.currentTimeMillis()antes y después. Verás la diferencia. - Avanza a la Sección 8 de UD2. Wrappers y la clase
Objects: las clasesInteger,Double,Boolean,Character; métodos útiles comoInteger.parseInt,Double.isNaN; el autoboxing/unboxing y sus riesgos; y los métodos estáticos deObjectscomorequireNonNulleisNull. Es una sección corta pero conceptualmente importante, porque cierra el bloque de "clases predefinidas del API de Java".