Imutabilidade de String em Java
Por que a classe String do Java é imutável — implicações para segurança, cache, hashing e segurança de threads.
Uma String em Java não pode ser alterada após sua criação. Uma vez que "hello" existe, nenhum método, nenhum truque de reflexão, nenhuma atribuição inteligente pode reescrever os caracteres daquele objeto específico. Toda operação que "modifica" uma string retorna na verdade uma nova String. A classe impõe isso: o campo que armazena os bytes é private final, a própria classe é final, e não existe setter público, append nem clear.
Essa escolha — imutabilidade — não é uma preferência estilística. É a decisão estrutural que torna o string pool seguro, o hashing confiável, o compartilhamento em múltiplas threads gratuito, e que viabiliza algumas garantias sutis de segurança.
O que "imutável" realmente significa
String s = "hello";
s.toUpperCase(); // returns "HELLO" — the return value is dropped
System.out.println(s); // prints "hello"
s = s.toUpperCase(); // s now *points at* a different String
System.out.println(s); // prints "HELLO"A variável s pode ser reatribuída — isso é uma propriedade da variável, não do objeto. O objeto criado originalmente com "hello" permanece inalterado, para sempre, independentemente do que s apontar depois. Se outra variável ainda faz referência a ele, essa variável continua vendo "hello".
String a = "hello";
String b = a;
a = a.toUpperCase();
System.out.println(a); // "HELLO"
System.out.println(b); // "hello" — still the originalÉ isso que as pessoas querem dizer quando dizem que strings são value-like: o conteúdo de uma referência String é tão estável quanto o conteúdo de um int.
Por que os designers da JVM escolheram a imutabilidade
Algumas propriedades decorrem da imutabilidade, e cada uma vale em termos de desempenho real ou segurança real.
O string pool é seguro. Se "hello" pudesse ser modificado no lugar, compartilhar uma instância do pool em todo o programa seria um desastre: alterá-la em um lugar mudaria silenciosamente em todos os outros. A imutabilidade é o que torna o string pool possível.
hashCode() pode ser armazenado em cache. String calcula seu hash na primeira chamada e armazena o resultado em um campo privado. Esse valor em cache seria uma mentira se os caracteres pudessem mudar depois, corrompendo todo HashMap<String, ?> com chave nessa string. Como o conteúdo é estável, o cache é permanente.
Leituras concorrentes não precisam de sincronização. Duas threads lendo a mesma referência String jamais observarão um valor parcialmente modificado. Não há synchronized, volatile nem barreira de memória — não há nada que possa mudar. Compare com um buffer mutável, em que seria necessário copiar, bloquear ou restringir a propriedade.
Carregamento de classes, reflexão e verificações de segurança podem confiar nos argumentos string. Um ClassLoader resolve nomes de classe a partir de Strings passadas pelo chamador. Se a string pudesse ser modificada por outra thread entre a verificação de segurança e a abertura do arquivo, haveria uma vulnerabilidade de condição de corrida — o clássico bug de tempo de verificação / tempo de uso. Com strings imutáveis, o valor validado é idêntico ao valor usado.
Argumentos de método não precisam de cópias defensivas. Ao passar uma String para um método, você não precisa se preocupar que ela seja mutada e te surpreenda no retorno. O receptor pode armazenar a referência diretamente; o chamador pode continuar usando sua referência também.
O custo: mutação em massa é cara
Há um preço. Construir uma string de 10.000 caracteres um caractere por vez com += aloca uma nova String a cada passo, copiando todos os caracteres que já existem mais o novo. Isso é trabalho quadrático — O(n²) para uma tarefa O(n).
// Don't do this for large n
String s = "";
for (int i = 0; i < n; i++) {
s += i + ",";
}A resposta da biblioteca padrão são buffers mutáveis — StringBuilder para código single-threaded e StringBuffer para o raro caso compartilhado. Eles mantêm um array redimensionável, fazem append em O(1) amortizado e produzem uma única String imutável no final com toString(). Esse é o padrão canônico para montar strings.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(i).append(',');
}
String s = sb.toString();Os JDKs modernos otimizam cadeias + curtas e de forma estaticamente definida via StringConcatFactory, então "hello, " + name + "!" está correto. O caso a evitar é += dentro de um loop com contagem desconhecida.
Tentando quebrá-la
A reflexão pode tecnicamente acessar o campo value privado e substituí-lo. Fazer isso é comportamento indefinido do ponto de vista da JVM: o JIT assume que strings são imutáveis e irá embutir o hashCode em cache, compartilhar referências pelo pool e pular barreiras de leitura com base nessa promessa. Mutar uma String via reflexão pode corromper silenciosamente código não relacionado que mantém uma referência ao mesmo objeto. Não faça isso. Se você precisar de mutabilidade, use StringBuilder.
Implicações de segurança
Dois casos concretos onde a imutabilidade importa para a segurança:
- Caminhos de arquivo e nomes de classe. Passados para APIs que realizam uma verificação de acesso antes de abrir ou carregar. Se um caminho pudesse mudar entre a verificação e o uso, sandboxes seriam contornáveis.
- Chaves de
ClassLoadere chaves de mapaString. Hash codes estáveis significam que um invasor não pode criar uma chave que "cabe" em um lugar e silenciosamente se realoca em outro.
O outro lado: armazenar senhas em uma String é uma prática ruim pelo motivo oposto. Uma vez que uma senha está em uma String, você não pode zerá-la — os bytes permanecem na memória heap até o GC recuperá-los, possivelmente depois que um dump de heap foi gravado. Para senhas, use char[] (que você pode preencher manualmente com zeros) ou — melhor ainda — javax.crypto.SecretKey e afins. O Console.readPassword() do JDK retorna char[] precisamente por esse motivo.
Um exemplo prático
Este programa cria uma string, a passa para vários chamadores, faz cada um "mutá-la" e imprime o que cada variável vê depois. O objeto original é visitado por quatro referências e sobrevive inalterado. O único buffer mutável no final é a alternativa canônica quando você genuinamente precisa construir uma string.
Observe as duas comparações ==. original e alias são literalmente o mesmo objeto, então a identidade se mantém. original e upper têm conteúdo relacionado, mas upper é um objeto novo — não há como upperCase ter alterado o objeto que recebeu. Essa é a garantia em que todo desenvolvedor Java se apoia sem pensar.
O que vem a seguir
Quando você precisar de uma string que possa alterar, a biblioteca padrão tem uma prima mutável de String. É a força de trabalho por trás de cada cadeia + que o compilador otimiza e a resposta certa sempre que você recorreria a += em um loop. Continue para Java StringBuilder.