StringBuilder vs StringBuffer: ¿cuál elegir y por qué es importante?
\n\nSi alguna vez has escrito código en Java que trabaja activamente con cadenas (por ejemplo, construyendo una consulta SQL, generando JSON o simplemente concatenando decenas de fragmentos de texto), seguramente te has enfrentado al dilema: ¿StringBuilder o StringBuffer? En Stack Overflow, esta pregunta ha acumulado cientos de miles de visitas, y no es para menos. A simple vista, las clases son como gemelos: ambas extienden AbstractStringBuilder, ambas usan un arreglo mutable de caracteres, ambas tienen métodos append(), insert(), delete(). Pero el diablo, como siempre, está en los detalles.
En este artículo no solo enumeraremos las diferencias. Las analizaremos con ejemplos reales, exploraremos el interior de la JVM y responderemos las preguntas que seguramente te harán en una entrevista. ¡Allá vamos!
\n\n1. La diferencia principal: sincronización
\n\nEste es el punto más importante, la razón de toda la discusión. StringBuffer es seguro para hilos (thread-safe). Casi todos sus métodos públicos están sincronizados con la palabra clave synchronized. StringBuilder no está sincronizado. Eso es todo.
Veamos el código (versión simplificada del JDK real):
\n\n// StringBuffer (Java 17)\npublic synchronized StringBuffer append(String str) {\n toStringCache = null;\n super.append(str);\n return this;\n}\n\n// StringBuilder\npublic StringBuilder append(String str) {\n super.append(str);\n return this;\n}\n\n\nUna sola línea synchronized marca toda la diferencia. Pero esa línea lo cambia todo: rendimiento, ámbito de aplicación, incluso la arquitectura de la aplicación.
¿Cuándo usar StringBuffer?
\n- \n
- Estás escribiendo una aplicación multihilo. \n
- La misma instancia de
StringBufferse usa desde varios hilos sin sincronización externa. \n - Ejemplo: un logger que recopila mensajes de diferentes hilos en un solo búfer. \n
¿Cuándo usar StringBuilder?
\n- \n
- Código de un solo hilo (90% de los casos). \n
- Variables locales dentro de un método. \n
- Bucles con concatenación de cadenas (el compilador sugerirá StringBuilder por sí mismo). \n
2. Rendimiento: ¿quién es más rápido?
\n\nAquí todo es predecible: StringBuilder es más rápido que StringBuffer. ¿Cuánto? Depende de la carga, pero en promedio es de 2 a 5 veces más rápido. Vamos a comprobarlo con un benchmark simple.
\n\npublic class PerformanceTest {\n private static final int ITERATIONS = 10_000_000;\n\n public static void main(String[] args) {\n // StringBuilder\n long start = System.nanoTime();\n StringBuilder sb = new StringBuilder();\n for (int i = 0; i < ITERATIONS; i++) {\n sb.append("a");\n }\n long sbTime = System.nanoTime() - start;\n\n // StringBuffer\n start = System.nanoTime();\n StringBuffer sbf = new StringBuffer();\n for (int i = 0; i < ITERATIONS; i++) {\n sbf.append("a");\n }\n long sbfTime = System.nanoTime() - start;\n\n System.out.println("StringBuilder: " + sbTime / 1_000_000 + " ms");\n System.out.println("StringBuffer: " + sbfTime / 1_000_000 + " ms");\n System.out.println("Buffer es " + (sbfTime / (double) sbTime) + " veces más lento");\n }\n}\n\n\nEn mi portátil (Intel i7, 16GB RAM, Java 17) el resultado es consistente: StringBuilder ~180 ms, StringBuffer ~620 ms. La diferencia es de casi 3.5 veces. Y si añadimos trabajo real (análisis, formato), la brecha puede aumentar. ¿Por qué? Porque cada llamada a synchronized implica verificar el monitor, adquirir el bloqueo y liberarlo. En código de un solo hilo, esto es una pérdida de tiempo pura.