StringBuilder vs StringBuffer: o que escolher e por que isso é importante?
\n\nSe você já escreveu código Java que trabalha intensamente com strings (por exemplo, montando uma consulta SQL, gerando JSON ou simplesmente concatenando dezenas de fragmentos de texto), certamente já enfrentou o dilema: StringBuilder ou StringBuffer? No Stack Overflow, essa pergunta acumulou centenas de milhares de visualizações, e não é à toa. À primeira vista, as classes são como gêmeas — ambas estendem AbstractStringBuilder, ambas usam um array de caracteres mutável, ambas possuem append(), insert(), delete(). Mas o diabo, como sempre, está nos detalhes.
Neste artigo, não vamos apenas listar as diferenças. Vamos analisá-las com exemplos reais, explorar os bastidores da JVM e responder às perguntas que certamente serão feitas em uma entrevista. Vamos lá!
\n\n1. A principal diferença: sincronização
\n\nEste é o ponto mais importante, a razão de toda a discussão. StringBuffer é thread-safe. Quase todos os seus métodos públicos são sincronizados com a palavra-chave synchronized. StringBuilder não é sincronizado. Ponto final.
Vamos dar uma olhada no código (versão simplificada do 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\nUma única linha synchronized — e toda a diferença. Mas essa linha muda tudo: desempenho, escopo de aplicação, até mesmo a arquitetura da aplicação.
Quando usar StringBuffer?
\n- \n
- Você está escrevendo uma aplicação multithread. \n
- A mesma instância de
StringBufferé usada por várias threads sem sincronização externa. \n - Exemplo: um logger que coleta mensagens de diferentes threads em um único buffer. \n
Quando usar StringBuilder?
\n- \n
- Código single-thread (90% dos casos). \n
- Variáveis locais dentro de um método. \n
- Loops com concatenação de strings (o compilador sugere StringBuilder automaticamente). \n
2. Desempenho: quem é mais rápido?
\n\nAqui tudo é previsível: StringBuilder é mais rápido que StringBuffer. Quanto? Depende da carga, mas em média de 2 a 5 vezes. Vamos verificar com um benchmark simples.
\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 é " + (sbfTime / (double) sbTime) + " vezes mais lento");\n }\n}\n\n\nNo meu notebook (Intel i7, 16GB RAM, Java 17), o resultado é consistente: StringBuilder ~180 ms, StringBuffer ~620 ms. A diferença é de quase 3,5 vezes. E se adicionarmos trabalho real (parsing, formatação), a diferença pode aumentar. Por quê? Porque cada chamada synchronized envolve verificação do monitor, aquisição e liberação do bloqueio. Em código single-thread, isso é pura perda de tempo.