Java Internals: O Custo Cognitivo da Imutabilidade de Strings

Introdução
Você já fez code review e encontrou algo assim?
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">public</span></span> <span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">static</span></span> String <span class=<span class="hljs-string">"hljs-title function_"</span>>stopIt</span><span class=<span class="hljs-string">"hljs-params"</span>>(String str)</span> {
str.replace(str, <span class=<span class="hljs-string">"hljs-string"</span>>&quot;stop&quot;</span>); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// parece certo...</span></span>
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">return</span></span> str; <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// mas retorna &quot;start&quot;</span></span>
}
A primeira reação costuma ser: "como esse dev não sabia que String é imutável?"
A resposta honesta é: ele provavelmente sabia. O problema não é falta de conhecimento - é o que podemos chamar de custo cognitivo da semântica verbal.
O Problema: a Semântica dos Verbos
Verbos como replace, trim, toUpperCase e toLowerCase carregam uma carga semântica de ação direta. O cérebro os processa como modificadores - da mesma forma que list.add() ou map.put() realmente alteram o estado do objeto.
Essa é a armadilha. Não é que o desenvolvedor esqueceu a imutabilidade. É que a leitura fluida do código ativa um padrão cognitivo diferente da realidade da JVM.
Veja a diferença de comportamento com coleções mutáveis:
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// List é mutável - add() modifica o estado diretamente</span></span>
List&lt;String&gt; list = <span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">new</span></span> <span class=<span class="hljs-string">"hljs-title class_"</span>>ArrayList</span>&lt;&gt;();
list.add(<span class=<span class="hljs-string">"hljs-string"</span>>&quot;item&quot;</span>); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// funciona como esperado</span></span>
System.out.println(list); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// [item]</span></span>
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// String é imutável - replace() não modifica nada</span></span>
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>texto</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-string"</span>>&quot;start&quot;</span>;
texto.replace(<span class=<span class="hljs-string">"hljs-string"</span>>&quot;start&quot;</span>, <span class=<span class="hljs-string">"hljs-string"</span>>&quot;stop&quot;</span>); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// resultado descartado silenciosamente</span></span>
System.out.println(texto); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// &quot;start&quot;</span></span>
O compilador não avisa. O IDE pode (e deve) alertar, mas em projetos com muitos warnings suprimidos, esse erro passa despercebido facilmente.
O que Acontece na JVM
Quando uma String é criada, seu conteúdo é alocado no String Constant Pool (SCP) - uma área especial da heap gerenciada pela JVM. Uma vez lá, o conteúdo é imutável e fixo.
Quando você chama str.replace(str, "stop"), a JVM:
- Lê o conteúdo original de
strno SCP - Cria um novo objeto String com o valor
"stop" - Retorna a referência para esse novo objeto
- Descarta a referência se você não a capturar
O objeto original não é tocado. A variável str continua apontando para "start".
SCP (String Constant Pool)
┌──────────────┐ ┌──────────────┐
│ &quot;start&quot; │ │ &quot;stop&quot; │
│ (imutável) │ │ (nova inst.) │
└──────┬───────┘ └──────┬───────┘
│ │
str ← referência descartada se não capturada
A Solução: Captura Explícita da Nova Referência
Métodos de manipulação de String não são mutators (modificadores de estado). São fábricas de novos objetos. Para que a alteração persista, a nova referência precisa ser explicitamente capturada:
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✗ Errado - resultado descartado</span></span>
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">public</span></span> <span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">static</span></span> String <span class=<span class="hljs-string">"hljs-title function_"</span>>stopIt</span><span class=<span class="hljs-string">"hljs-params"</span>>(String str)</span> {
str.replace(str, <span class=<span class="hljs-string">"hljs-string"</span>>&quot;stop&quot;</span>);
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">return</span></span> str; <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// retorna &quot;start&quot;</span></span>
}
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✓ Correto - nova instância capturada</span></span>
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">public</span></span> <span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">static</span></span> String <span class=<span class="hljs-string">"hljs-title function_"</span>>stopIt</span><span class=<span class="hljs-string">"hljs-params"</span>>(String str)</span> {
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">return</span></span> str.replace(str, <span class=<span class="hljs-string">"hljs-string"</span>>&quot;stop&quot;</span>); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// retorna &quot;stop&quot;</span></span>
}
O mesmo padrão se aplica a toda a API de String:
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>nome</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-string"</span>>&quot; guilherme &quot;</span>;
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✗ Errado</span></span>
nome.trim();
nome.toUpperCase();
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✓ Correto</span></span>
nome = nome.trim().toUpperCase();
System.out.println(nome); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// &quot;GUILHERME&quot;</span></span>
Por que a Imutabilidade Existe
A imutabilidade das Strings não é uma limitação - é uma decisão arquitetural deliberada que viabiliza otimizações críticas na JVM:
1. String Constant Pool e Compartilhamento de Referências
O SCP permite que literais idênticos compartilhem a mesma referência em memória:
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>a</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-string"</span>>&quot;Java&quot;</span>;
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>b</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-string"</span>>&quot;Java&quot;</span>;
System.out.println(a == b); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// true - mesma referência no SCP</span></span>
Se Strings fossem mutáveis, compartilhar referências seria perigoso: alterar a afetaria b.
2. Thread Safety Nativa
Strings são inerentemente thread-safe. Não há necessidade de synchronized, volatile ou locks para compartilhar uma String entre threads, porque nenhuma thread pode modificar seu conteúdo.
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// Seguro compartilhar entre threads sem qualquer sincronização</span></span>
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>endpoint</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-string"</span>>&quot;https:<span class="hljs-comment">//getcaramelo.dev/v1/posts&quot;</span>;</span>
<span class=<span class="hljs-string">"hljs-type"</span>>ExecutorService</span> <span class=<span class="hljs-string">"hljs-variable"</span>>pool</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> Executors.newVirtualThreadPerTaskExecutor();
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">for</span></span> (<span class=<span class="hljs-string">"hljs-type"</span>><span class="hljs-type">int</span></span> <span class=<span class="hljs-string">"hljs-variable"</span>>i</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-number"</span>><span class="hljs-number">0</span></span>; i &lt; <span class=<span class="hljs-string">"hljs-number"</span>><span class="hljs-number">100</span></span>; i++) {
pool.submit(() -&gt; fetch(endpoint)); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// endpoint é imutável, sem risco</span></span>
}
3. String Deduplication (G1GC)
O G1 Garbage Collector possui uma otimização chamada String Deduplication: ele identifica objetos String com conteúdo idêntico em diferentes regiões da heap e consolida suas referências para apontar ao mesmo char[] interno. Isso só é possível porque o conteúdo é imutável - a JVM tem a garantia de que ninguém vai alterá-lo por baixo dos panos.
4. HashCode Cacheado
O hashCode() de uma String é computado uma vez e armazenado internamente. Como o conteúdo nunca muda, o cache é sempre válido - o que torna operações com HashMap<String, ?> mais eficientes do que com objetos mutáveis equivalentes.
Quando Você Precisa de Mutabilidade: StringBuilder
Toda vez que você precisa construir uma String em múltiplos passos, o StringBuilder entra em cena - mas usá-lo em todos os casos é um exagero que pode custar em legibilidade sem ganho real de performance.
O Cenário Crítico: Concatenação em Loops
O uso do operador + dentro de um loop é o principal caso onde o StringBuilder deve ser preferido explicitamente. Cada iteração com + pode criar um novo objeto String intermediário na heap, gerando lixo que o GC precisará processar. Em volumes altos, isso resulta em complexidade de tempo O(n²):
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✗ Ineficiente - potencialmente O(n²)</span></span>
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// cada iteração cria um objeto String descartável</span></span>
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>resultado</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-string"</span>>&quot;&quot;</span>;
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">for</span></span> (String parte : partes) {
resultado += parte;
}
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✓ Eficiente - O(n)</span></span>
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// uma única instância de buffer, append in-place</span></span>
<span class=<span class="hljs-string">"hljs-type"</span>>StringBuilder</span> <span class=<span class="hljs-string">"hljs-variable"</span>>sb</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">new</span></span> <span class=<span class="hljs-string">"hljs-title class_"</span>>StringBuilder</span>();
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">for</span></span> (String parte : partes) {
sb.append(parte);
}
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>resultado</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> sb.toString();
O mesmo vale para construção condicional ao longo de múltiplos passos de lógica:
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✓ StringBuilder faz sentido aqui - construção incremental e condicional</span></span>
<span class=<span class="hljs-string">"hljs-type"</span>>StringBuilder</span> <span class=<span class="hljs-string">"hljs-variable"</span>>query</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">new</span></span> <span class=<span class="hljs-string">"hljs-title class_"</span>>StringBuilder</span>(<span class=<span class="hljs-string">"hljs-string"</span>>&quot;SELECT * FROM users WHERE <span class="hljs-number">1</span>=<span class="hljs-number">1</span>&quot;</span>);
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">if</span></span> (filtroNome != <span class=<span class="hljs-string">"hljs-literal"</span>><span class="hljs-literal">null</span></span>) query.append(<span class=<span class="hljs-string">"hljs-string"</span>>&quot; <span class="hljs-type">AND</span> <span class="hljs-variable">nome</span> <span class="hljs-operator">=</span> &#x27;&quot;</span>).append(filtroNome).append(<span class=<span class="hljs-string">"hljs-string"</span>>&quot;&#x27;&quot;</span>);
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">if</span></span> (filtroAtivo != <span class=<span class="hljs-string">"hljs-literal"</span>><span class="hljs-literal">null</span></span>) query.append(<span class=<span class="hljs-string">"hljs-string"</span>>&quot; <span class="hljs-type">AND</span> <span class="hljs-variable">ativo</span> <span class="hljs-operator">=</span> &quot;</span>).append(filtroAtivo);
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">if</span></span> (limite &gt; <span class=<span class="hljs-string">"hljs-number"</span>><span class="hljs-number">0</span></span>) query.append(<span class=<span class="hljs-string">"hljs-string"</span>>&quot; LIMIT &quot;</span>).append(limite);
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">return</span></span> query.toString();
Onde o + Vence: Concatenação Simples
Para concatenações diretas em uma única instrução, o compilador e a JVM já fazem o trabalho por você - e fazem bem.
No JDK 8, o compilador transformava internamente "A" + "B" + "C" em uma implementação de StringBuilder no bytecode. A partir do JDK 9, o JEP 280 introduziu a Indify String Concatenation: a instrução invokedynamic delega a operação à JVM de forma dinâmica, permitindo otimizações que nem aparecem no bytecode e que evoluem com as versões do Java sem recompilação.
Na prática, benchmarks em máquinas modernas mostram que a concatenação via + em casos simples pode ser mais rápida do que o uso explícito de StringBuilder:
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✓ Deixa o compilador/JVM otimizar - é a escolha certa aqui</span></span>
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>mensagem</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-string"</span>>&quot;Olá, &quot;</span> + nome + <span class=<span class="hljs-string">"hljs-string"</span>>&quot;! Bem-vindo ao &quot;</span> + plataforma + <span class=<span class="hljs-string">"hljs-string"</span>>&quot;.&quot;</span>;
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✗ Desnecessariamente verbose - sem ganho de performance</span></span>
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>mensagem</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">new</span></span> <span class=<span class="hljs-string">"hljs-title class_"</span>>StringBuilder</span>()
.append(<span class=<span class="hljs-string">"hljs-string"</span>>&quot;Olá, &quot;</span>).append(nome)
.append(<span class=<span class="hljs-string">"hljs-string"</span>>&quot;! Bem-vindo ao &quot;</span>).append(plataforma).append(<span class=<span class="hljs-string">"hljs-string"</span>>&quot;.&quot;</span>)
.toString();
Alternativas Modernas para Legibilidade
Quando o objetivo é legibilidade na construção de texto dinâmico - XML, JSON, SQL, templates - existem alternativas mais expressivas que o StringBuilder:
Text Blocks (JDK 15+) para strings multilinhas fixas:
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>json</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-string"</span>>&quot;&quot;&quot;
{
&quot;nome&quot;: &quot;%s&quot;,
&quot;ativo&quot;: %b
}
&quot;&quot;&quot;</span>.formatted(nome, ativo);
String.formatted() como alternativa legível ao String.format() - mesmo resultado, sintaxe mais fluida:
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// String.format() - clássico</span></span>
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>msg</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> String.format(<span class=<span class="hljs-string">"hljs-string"</span>>&quot;Usuário %s criado em %s&quot;</span>, nome, data);
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// String.formatted() - mais idiomático desde o Java 15</span></span>
<span class=<span class="hljs-string">"hljs-type"</span>>String</span> <span class=<span class="hljs-string">"hljs-variable"</span>>msg</span> <span class=<span class="hljs-string">"hljs-operator"</span>>=</span> <span class=<span class="hljs-string">"hljs-string"</span>>&quot;Usuário %s criado em %s&quot;</span>.formatted(nome, data);
⚠️ Evite
String.format()eString.formatted()dentro de loops de alta frequência. Ambos são consideravelmente mais lentos queStringBuilderou concatenação simples por envolverem parsing de formato a cada chamada.
Guia de Decisão Rápido
| Cenário | Use |
|---|---|
| Concatenar poucas variáveis em uma linha | + ou String.formatted() |
| Construir String dentro de loop | StringBuilder |
| Construção incremental com condicionais | StringBuilder |
| Template multiline com valores fixos | Text Block |
| Template multiline com interpolação | Text Block + .formatted() |
| Alta frequência em loop crítico | StringBuilder (evite format) |
Revisitando o Código Original
Com tudo isso em mente, o erro inicial fica mais claro:
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✗ Versão com o erro</span></span>
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">public</span></span> <span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">static</span></span> String <span class=<span class="hljs-string">"hljs-title function_"</span>>stopIt</span><span class=<span class="hljs-string">"hljs-params"</span>>(String str)</span> {
str.replace(str, <span class=<span class="hljs-string">"hljs-string"</span>>&quot;stop&quot;</span>); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// cria &quot;stop&quot; na heap, referência perdida</span></span>
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">return</span></span> str; <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// str ainda aponta para &quot;start&quot; no SCP</span></span>
}
<span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// ✓ Versão correta</span></span>
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">public</span></span> <span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">static</span></span> String <span class=<span class="hljs-string">"hljs-title function_"</span>>stopIt</span><span class=<span class="hljs-string">"hljs-params"</span>>(String str)</span> {
<span class=<span class="hljs-string">"hljs-keyword"</span>><span class="hljs-keyword">return</span></span> str.replace(str, <span class=<span class="hljs-string">"hljs-string"</span>>&quot;stop&quot;</span>); <span class=<span class="hljs-string">"hljs-comment"</span>><span class="hljs-comment">// captura e retorna a nova referência</span></span>
}
O erro não é conceitual - é de leitura. replace soa como uma operação in-place. Para quem lê rápido (e revisores de PR leem rápido), o olho passa e o cérebro valida. Saber disso é o que separa um bom code reviewer de um revisor médio.
Conclusão
Imutabilidade de Strings em Java não é um detalhe trivial de documentação. É uma decisão que impacta gerenciamento de memória, segurança em concorrência e performance do GC. O custo cognitivo existe - e conhecê-lo te torna mais atento tanto ao escrever quanto ao revisar código.
Na próxima vez que você ver um verbo como replace, trim ou toUpperCase em uma revisão, a pergunta certa é simples: o resultado foi capturado?
Este artigo faz parte da série Objects · Immutability · Switch · Pattern Matching do getcaramelo.dev.
Tem dúvidas ou quer aprofundar algum ponto? Me encontra no LinkedIn ou no GitHub.
Publicado por: Guilherme Gomes - 28/06/2026 15:10
Gostou do conteúdo? Me segue no @getcaramelo.dev
Caramelo.dev