Java StringBuilder
Crea cadenas mutables eficientemente en Java con StringBuilder: append, insert, reverse y más.
Un String es inmutable; crecer uno con += en un bucle es cuadrático. StringBuilder es la respuesta de la biblioteca estándar: un único objeto con un buffer interno redimensionable que permite añadir, insertar, eliminar e invertir contenido en su lugar, para luego convertirlo en un String inmutable al final. Es la herramienta fundamental detrás de todos los patrones modernos de construcción de cadenas en el JDK, y la opción correcta en cuanto te encuentras acumulando texto en un bucle o a través de llamadas a métodos.
Construir un builder
StringBuilder sb = new StringBuilder(); // empty, capacity 16
StringBuilder withCap = new StringBuilder(1024); // empty, preallocated capacity
StringBuilder fromText = new StringBuilder("hi "); // capacity = length + 16El constructor sin argumentos comienza con capacidad 16, lo cual está bien para resultados cortos y es una elección pesimista para los largos. Si sabes aproximadamente el tamaño del string final, pasa la capacidad desde el principio — cada expansión evitada es una asignación de array menos y un arraycopy menos. new StringBuilder(estimatedLength) es la microoptimización más efectiva de toda esta parte del libro.
Encadenamiento: cada mutador retorna this
Cada método mutador de StringBuilder retorna el propio builder, de modo que las llamadas se componen en una sola expresión:
String greeting = new StringBuilder()
.append("Hello, ")
.append(name)
.append('!')
.append('\n')
.toString();Esto es una convención, no magia; podrías separarlo en múltiples sentencias sin ninguna diferencia funcional. El estilo encadenado refleja cómo el compilador reescribe las cadenas de + internamente, lo cual es precisamente la razón por la que la forma encadenada resulta natural en Java.
La familia de mutadores
StringBuilder tiene una superficie pequeña y enfocada:
append— añade texto o cualquier tipo primitivo al final. Sobrecargado para todos los primitivos,char[],CharSequenceyObject(llama atoString).insert(offset, ...)— las mismas sobrecargas, pero en una posición arbitraria.delete(start, end),deleteCharAt(i)— elimina un rango o un único carácter.replace(start, end, replacement)— reemplaza un rango con una nueva subcadena; las longitudes pueden diferir.reverse()— invierte el buffer en su lugar.setCharAt(i, ch)— reescribe un único carácter.setLength(n)— trunca (o rellena con) hastancaracteres.
Estos métodos editan el buffer; no retornan un nuevo String. Para obtener una instantánea del contenido como string inmutable, llama a toString().
Inspeccionar y convertir
length()— número actual de caracteres.capacity()— tamaño actual del array interno. Siempre ≥length().charAt(i),substring(start)/substring(start, end)— acceso de lectura, idéntico aString.indexOf(s),lastIndexOf(s)— localiza una subcadena.toString()— produce unStringinmutable. Llámalo una sola vez al final; llamarlo repetidamente durante una construcción es una asignación innecesaria.ensureCapacity(n)— hace crecer previamente el buffer hasta al menosn.trimToSize()— reduce el buffer para ajustarse al contenido actual. Rara vez se necesita.
Cómo crece el buffer
Internamente, StringBuilder mantiene un byte[] (o char[] en JDKs más antiguos). Cuando un append desbordaría el buffer, este se reasigna a aproximadamente 2 × oldCapacity + 2, y el contenido anterior se copia. Cada expansión es O(n) en el tamaño actual, pero el patrón de duplicación hace que el costo total de n appends sea O(n) amortizado — muy diferente a la concatenación repetida de String, donde el mismo bucle es O(n²).
append "a" — capacity 16, length 1
... 15 more — capacity 16, length 16
append "b" — grow to 34, length 17
... 17 more — capacity 34, length 34
append "c" — grow to 70, length 35Si conoces la longitud final, te saltas todas esas reasignaciones construyendo directamente con esa capacidad.
StringBuilder vs el operador +
Para ensamblaje de strings cortos y estáticamente conocidos, el compilador hace lo correcto por sí solo. "Hello, " + name + "!" se reescribe en tiempo de compilación como una sola cadena de StringBuilder o como una llamada a StringConcatFactory.makeConcatWithConstants (Java 9+). Ambas son eficientes. No necesitas gestionar estas expresiones de manera manual.
El patrón a evitar es += dentro de un bucle sobre un número desconocido de iteraciones:
// O(n²) — every += allocates a new String holding everything seen so far
String out = "";
for (String token : tokens) {
out += token + "|";
}
// O(n) — one buffer, one final String
StringBuilder sb = new StringBuilder();
for (String token : tokens) {
sb.append(token).append('|');
}
String out = sb.toString();Si el bucle se ejecuta unas pocas veces, la diferencia es invisible. Con unos pocos miles de tokens, es un problema medible en benchmarks. Con un millón, la primera forma se cuelga y la segunda tarda milisegundos.
StringBuilder no es thread-safe
StringBuilder omite deliberadamente la sincronización para ser rápido en el caso predominantemente monohilo. Si dos hilos hacen append al mismo builder simultáneamente, los resultados son indefinidos: escrituras perdidas, caracteres sobreescritos, o una ArrayIndexOutOfBoundsException desde la ruta de expansión. Para el caso excepcional en que un builder se comparte entre hilos, usa su gemelo sincronizado — StringBuffer — en su lugar. En la práctica casi nunca se comparte un builder; cada hilo construye el suyo propio.
Un ejemplo completo
El programa siguiente usa todas las partes de la superficie que importan en el código cotidiano: append encadenado, un insert explícito, un replace que cambia la longitud de un rango, un reverse, y un único toString final. La capacidad se imprime al inicio y al final para hacer visible la duplicación del buffer.
Observa los números de capacidad en la salida (capacity=32 al inicio, capacity=66 al final). Cada append de la cadena cabe dentro de la capacidad inicial de 32 — el string construido tiene solo 24 caracteres. El insert y el replace empujan entonces la longitud hasta 40, que desborda 32 y desencadena exactamente una expansión (a 2 × 32 + 2 = 66). Ajustar el tamaño más cerca de la longitud final real — new StringBuilder(48) en este caso — habría evitado esa única reasignación. Ese es todo el juego: cuanto mejor sea tu estimación de capacidad, menos eventos de copia en la expansión tendrás.
Qué sigue
StringBuilder es rápido porque no es thread-safe. Su gemelo sincronizado es la respuesta correcta en el caso (excepcional) en que realmente quieras un buffer compartido entre hilos — la misma API, pero con métodos que bloquean. Continúa con Java StringBuffer.