Uma string Java é realmente imutável?

Nós todos sabemos que String é imutável em Java, mas verifique o seguinte código:

 String s1 = "Hello World"; String s2 = "Hello World"; String s3 = s1.substring(6); System.out.println(s1); // Hello World System.out.println(s2); // Hello World System.out.println(s3); // World Field field = String.class.getDeclaredField("value"); field.setAccessible(true); char[] value = (char[])field.get(s1); value[6] = 'J'; value[7] = 'a'; value[8] = 'v'; value[9] = 'a'; value[10] = '!'; System.out.println(s1); // Hello Java! System.out.println(s2); // Hello Java! System.out.println(s3); // World 

Por que este programa funciona assim? E por que o valor de s1 e s2 alterado, mas não o s3 ?

String é imutável *, mas isso significa que você não pode alterá-lo usando sua API pública.

O que você está fazendo aqui é contornar a API normal, usando reflection. Da mesma forma, você pode alterar os valores de enums, alterar a tabela de pesquisa usada no Integer autoboxing etc.

Agora, a razão s1 e s2 alteram o valor, é que ambos se referem à mesma cadeia de caracteres internada. O compilador faz isso (como mencionado por outras respostas).

A razão pela qual o s3 não foi realmente um pouco surpreendente para mim, pois achei que ele compartilharia o array value ( ele fez na versão anterior do Java , antes do Java 7u6). No entanto, olhando para o código-fonte de String , podemos ver que a matriz de caracteres de value para uma substring é realmente copiada (usando Arrays.copyOfRange(..) ). É por isso que permanece inalterado.

Você pode instalar um SecurityManager , para evitar códigos maliciosos para fazer tais coisas. Mas tenha em mente que algumas bibliotecas dependem do uso desses tipos de truques de reflection (geralmente ferramentas ORM, bibliotecas AOP, etc.).

*) Eu inicialmente escrevi que String s não são realmente imutáveis, apenas “imutáveis ​​e eficazes”. Isso pode ser enganoso na implementação atual de String , em que a matriz de value é realmente marcada como private final . No entanto, vale a pena observar que não há como declarar um array em Java como imutável, portanto, deve-se tomar cuidado para não expô-lo fora de sua class, mesmo com os modificadores de access adequados.


Como este tópico parece extremamente popular, eis algumas sugestões de outras leituras: a palestra de Heinz Kabutz sobre Reflexão na Loucura, do JavaZone 2009, que cobre muitas das questões do OP, junto com outras reflexões … bem … loucura.

Abrange porque isso às vezes é útil. E por que, na maioria das vezes, você deve evitá-lo. 🙂

Em Java, se duas variables ​​primitivas de cadeia forem inicializadas para o mesmo literal, ele atribui a mesma referência às duas variables:

 String Test1="Hello World"; String Test2="Hello World"; System.out.println(test1==test2); // true 

inicialização

Essa é a razão pela qual a comparação retorna verdadeira. A terceira string é criada usando substring() que cria uma nova string em vez de apontar para a mesma.

sub string

Quando você acessa uma string usando reflexo, você obtém o ponteiro real:

 Field field = String.class.getDeclaredField("value"); field.setAccessible(true); 

Então mude para isso irá mudar a string segurando um ponteiro para ele, mas como s3 é criado com uma nova string devido a substring() ela não mudaria.

mudança

Você está usando a reflection para contornar a imutabilidade da String – é uma forma de “ataque”.

Existem muitos exemplos que você pode criar assim (por exemplo, você também pode instanciar um object Void ), mas isso não significa que o String não é “imutável”.

Existem casos de uso em que esse tipo de código pode ser usado para sua vantagem e ter uma “boa codificação”, como limpar senhas da memory o mais cedo possível (antes do GC) .

Dependendo do gerenciador de segurança, você não poderá executar seu código.

Você está usando reflection para acessar os “detalhes de implementação” do object string. Imutabilidade é a característica da interface pública de um object.

Modificadores de visibilidade e final (ou seja, imutabilidade) não são uma medida contra código malicioso em Java; eles são meramente ferramentas para proteger contra erros e para tornar o código mais sustentável (um dos grandes pontos de venda do sistema). É por isso que você pode acessar detalhes de implementação internos, como a matriz de caracteres de backup para String s por reflection.

O segundo efeito que você vê é que todas as String s mudam enquanto parece que você só muda s1 . É uma certa propriedade dos literais Java String que eles são automaticamente internados, ou seja, armazenados em cache. Dois literais de string com o mesmo valor serão, na verdade, o mesmo object. Quando você cria uma String com new ela não será internada automaticamente e você não verá este efeito.

#substring até recentemente (Java 7u6) funcionava de forma semelhante, o que teria explicado o comportamento na versão original da sua pergunta. Ele não criou uma nova matriz de caracteres de suporte, mas reutilizou a da String original; ele apenas criou um novo object String que usou um deslocamento e um comprimento para apresentar apenas uma parte desse array. Isso geralmente funciona como Strings são imutáveis ​​- a menos que você contorne isso. Essa propriedade de #substring também significava que a String original inteira não poderia ser coletada quando uma subseqüência menor criada a partir dela ainda existia.

A partir do Java atual e da sua versão atual da questão, não há comportamento estranho de #substring .

A imutabilidade da string é da perspectiva da interface. Você está usando reflection para ignorar a interface e modificar diretamente os componentes internos das instâncias String.

s1 e s2 são ambos alterados porque ambos são atribuídos à mesma instância de string “intern”. Você pode descobrir um pouco mais sobre essa parte deste artigo sobre igualdade e internação de strings. Você pode se surpreender ao descobrir que no seu código de exemplo, s1 == s2 retorna true !

Qual versão do Java você está usando? A partir do Java 1.7.0_06, o Oracle alterou a representação interna de String, especialmente a substring.

Citando a Representação de Seqüências Internas do Oracle Tunes Java :

No novo paradigma, os campos Deslocamento de seqüência e contagem foram removidos, portanto, substrings não compartilham mais o valor char [] subjacente.

Com essa alteração, isso pode acontecer sem reflection (???).

Existem realmente duas perguntas aqui:

  1. As cordas são realmente imutáveis?
  2. Por que s3 não é alterado?

Para o ponto 1: Exceto para ROM, não há memory imutável em seu computador. Hoje em dia, mesmo a ROM é por vezes gravável. Há sempre algum código em algum lugar (seja o kernel ou o código nativo contornando o ambiente gerenciado) que pode gravar em seu endereço de memory. Então, na “realidade”, não, eles não são absolutamente imutáveis.

Para o ponto 2: Isso ocorre porque a substring provavelmente está alocando uma nova instância de cadeia de caracteres, o que provavelmente copia a matriz. É possível implementar substring de tal forma que não faça uma cópia, mas isso não significa que ele faça. Existem tradeoffs envolvidos.

Por exemplo, deveria manter uma referência a reallyLargeString.substring(reallyLargeString.length - 2) causar uma grande quantidade de memory para ser mantida viva, ou apenas alguns bytes?

Isso depende de como a substring é implementada. Uma cópia profunda manterá menos memory ativa, mas ficará um pouco mais lenta. Uma cópia superficial manterá mais memory ativa, mas será mais rápida. O uso de uma cópia detalhada também pode reduzir a fragmentação de heap, já que o object de cadeia de caracteres e seu buffer podem ser alocados em um bloco, em oposição a duas alocações de heap separadas.

Em qualquer caso, parece que sua JVM escolheu usar as cópias profundas para chamadas de substring.

Para adicionar à resposta do @ haraldK – este é um hack de segurança que pode levar a um sério impacto no aplicativo.

A primeira coisa é uma modificação em uma string constante armazenada em um pool de strings. Quando string é declarado como String s = "Hello World"; está sendo colocado em um pool de objects especiais para uma possível reutilização. O problema é que o compilador colocará uma referência à versão modificada no momento da compilation e, uma vez que o usuário modifique a string armazenada nesse pool em tempo de execução, todas as referências no código apontarão para a versão modificada. Isso resultaria em um bug seguinte:

 System.out.println("Hello World"); 

Vai imprimir:

 Hello Java! 

Houve outro problema que experimentei quando estava implementando uma computação pesada sobre essas cadeias de caracteres arriscadas. Houve um bug que aconteceu em 1 de 1000000 vezes durante o cálculo, o que tornou o resultado indeterminado. Consegui encontrar o problema desligando o JIT – estava sempre obtendo o mesmo resultado com o JIT desligado. Meu palpite é que o motivo foi esse hack de segurança String que quebrou alguns dos contratos de otimização JIT.

De acordo com o conceito de pooling, todas as variables ​​String contendo o mesmo valor apontarão para o mesmo endereço de memory. Portanto, s1 e s2, ambos contendo o mesmo valor de “Hello World”, apontarão para o mesmo local de memory (por exemplo, M1).

Por outro lado, s3 contém “Mundo”, portanto, apontará para uma alocação de memory diferente (por exemplo, M2).

Então agora o que está acontecendo é que o valor de S1 está sendo alterado (usando o valor char []). Portanto, o valor no local de memory M1 apontado por s1 e s2 foi alterado.

Assim, como resultado, a localização da memory M1 foi modificada, o que causa uma alteração no valor de s1 e s2.

Mas o valor da localização M2 permanece inalterado, portanto s3 contém o mesmo valor original.

A razão pela qual o s3 não muda realmente é porque em Java, quando você faz uma subseqüência, a matriz de caracteres de valor para uma substring é copiada internamente (usando Arrays.copyOfRange ()).

s1 e s2 são os mesmos porque, em Java, ambos se referem à mesma cadeia de caracteres internada. É por design em Java.

String é imutável, mas através da reflection você pode alterar a class String. Você acabou de redefinir a class String como mutável em tempo real. Você poderia redefinir os methods para serem públicos, privados ou estáticos, se quisesse.

[Disclaimer este é um estilo deliberadamente opinativo de resposta como eu sinto uma resposta mais “não faça isso em casa crianças” é justificada]

O pecado é a linha field.setAccessible(true); que diz violar a API pública, permitindo o access a um campo privado. Isso é um buraco de segurança gigante que pode ser bloqueado configurando um gerenciador de segurança.

O fenômeno na questão são detalhes de implementação que você nunca veria quando não usasse essa linha de código perigosa para violar os modificadores de access por meio da reflection. Claramente, duas cadeias (normalmente) imutáveis ​​podem compartilhar a mesma matriz de caracteres. Se uma substring compartilha a mesma matriz depende se ela pode ou se o desenvolvedor pensou em compartilhá-la. Normalmente, esses são detalhes de implementação invisíveis que você não deveria precisar saber, a menos que você atire no modificador de access pela cabeça com essa linha de código.

Simplesmente não é uma boa idéia confiar em tais detalhes que não podem ser experimentados sem violar os modificadores de access usando reflection. O proprietário dessa class suporta apenas a API pública normal e está livre para fazer alterações de implementação no futuro.

Tendo dito tudo o que a linha de código é realmente muito útil quando você tem uma arma segurou sua cabeça forçando você a fazer coisas tão perigosas. Usar essa porta dos fundos geralmente é um cheiro de código que você precisa atualizar para melhorar o código da biblioteca, onde você não precisa pecar. Outro uso comum dessa linha perigosa de código é escrever um “framework voodoo” (orm, container de injeção, …). Muitas pessoas se tornam religiosas sobre tais estruturas (tanto a favor quanto contra elas), então evitarei convidar uma guerra de chamas dizendo que nada além da vasta maioria dos programadores não precisa ir até lá.

As cadeias são criadas na área permanente da memory heap da JVM. Então, sim, é realmente imutável e não pode ser alterado depois de criado. Como na JVM, existem três tipos de memory heap: 1. Geração jovem 2. Geração antiga 3. Geração permanente.

Quando qualquer object é criado, ele entra na área de heap da nova geração e na área PermGen reservada para o conjunto de Sequências.

Aqui está mais detalhes, você pode ir e pegar mais informações de: Como o Garbage Collection funciona em Java .