Uma string Java é realmente imutável?


399

Nós todos sabemos isso 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 s1e s2mudado, mas não s3?


394
Você pode fazer todos os tipos de truques estúpidos com reflexão. Mas você está basicamente quebrando o adesivo "garantia nula se removida" na classe no instante em que você faz isso.
cHao

16
@DarshanPatel usar um SecurityManager para desativar reflexão
Sean Patrick Floyd

39
Se você realmente quer mexer com as coisas, pode fazê-lo de maneira que (Integer)1+(Integer)2=42mexa com a caixa automática em cache; (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )
Richard Tingle

15
Você pode se divertir com esta resposta que escrevi há quase 5 anos stackoverflow.com/a/1232332/27423 - trata-se de listas imutáveis ​​em C #, mas é basicamente a mesma coisa: como posso impedir que os usuários modifiquem meus dados? E a resposta é: você não pode; reflexão torna muito fácil. Uma linguagem convencional que não tem esse problema é o JavaScript, pois não possui um sistema de reflexão que possa acessar variáveis ​​locais dentro de um fechamento, portanto, privado realmente significa privado (mesmo que não exista uma palavra-chave para ele!)
Daniel Earwicker

49
Alguém está lendo a pergunta até o fim? A questão é, deixe-me repetir: "Por que este programa funciona assim? Por que o valor de s1 e s2 foi alterado e não foi alterado para s3?" A questão NÃO é por que s1 e s2 foram alterados! A pergunta É: POR QUE o s3 não foi alterado?
Roland Pihlakas

Respostas:


403

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

O que você está fazendo aqui é contornar a API normal, usando reflexão. Da mesma forma, você pode alterar os valores de enumerações, alterar a tabela de pesquisa usada na caixa automática de números inteiros etc.

Agora, a razão s1es2 valor da mudança, é que ambos se referem à mesma cadeia interna. O compilador faz isso (conforme mencionado por outras respostas).

A razão s3é que não foi realmente um pouco surpreendente para mim, como eu pensei que iria partilhar o valuearray ( ele fez na versão anterior do Java , antes de 7u6 Java). No entanto, observando o código-fonte de String, podemos ver que a valuematriz de caracteres para uma substring é realmente copiada (usandoArrays.copyOfRange(..) ). É por isso que permanece inalterado.

Você pode instalar um SecurityManager , para evitar código malicioso, para fazer essas coisas. Mas lembre-se de que algumas bibliotecas dependem do uso desses tipos de truques de reflexão (normalmente ferramentas ORM, bibliotecas AOP etc.).

*) Eu escrevi inicialmente que Strings não são realmente imutáveis, apenas "imutáveis ​​efetivamente". Isso pode ser enganoso na implementação atual de String, onde a valuematriz é realmente marcada private final. Ainda vale a pena notar, no entanto, que não há como declarar uma matriz em Java como imutável; portanto, deve-se tomar cuidado para não expô-la fora de sua classe, mesmo com os modificadores de acesso adequados.


Como esse tópico parece extremamente popular, aqui estão algumas leituras adicionais sugeridas: Palestra de Reflection Madness de Heinz Kabutz do JavaZone 2009, que aborda muitos dos problemas do OP, além de outras reflexões ... bem ... loucura.

Ele aborda por que isso às vezes é útil. E por que, na maioria das vezes, você deve evitá-lo. :-)


7
Na verdade, a Stringinternação faz parte do JLS ( "um literal de string sempre se refere à mesma instância da classe String" ). Mas eu concordo, não é uma boa prática contar com os detalhes de implementação da Stringclasse.
haraldK

3
Talvez a razão pela qual as substringcópias, em vez de usar uma "seção" da matriz existente, seja de outro modo, se eu tivesse uma string enorme se retirasse uma pequena substring t, e depois eu a abandonasse, smas a mantivesse t, a enorme variedade seria mantida viva (não coleta de lixo). Então, talvez seja mais natural que cada valor de string tenha sua própria matriz associada?
Jeppe Stig Nielsen

10
Compartilhar matrizes entre uma string e suas substrings também implicava que todas as String instâncias precisavam carregar variáveis ​​para lembrar o deslocamento na matriz e no comprimento referidos. Essa é uma sobrecarga a não ignorar, dado o número total de seqüências de caracteres e a proporção típica entre seqüências de caracteres normais e substrings em um aplicativo. Como eles precisavam ser avaliados para cada operação de cadeia, isso significava abrandar cada operação de cadeia apenas para o benefício de apenas uma operação, uma substring barata.
Holger

2
@ Holger - Sim, meu entendimento é que o campo de deslocamento foi eliminado nas JVMs recentes. E mesmo quando estava presente, não era usado com tanta frequência.
Hot Licks

2
@ supercat: não importa se você tem código nativo ou não, tendo implementações diferentes para cadeias de caracteres e substring na mesma JVM ou com byte[]cadeias de caracteres para cadeias ASCII e char[]para outras, implica que toda operação deve verificar qual tipo de cadeia é anterior operativo. Isso dificulta a inserção do código nos métodos usando strings, que é o primeiro passo de otimizações adicionais usando as informações de contexto do chamador. Este é um grande impacto.
Holger

93

Em Java, se duas variáveis ​​primitivas de cadeia de caracteres são inicializadas no mesmo literal, ele atribui a mesma referência às duas variáveis:

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 o substring()que cria uma nova string em vez de apontar para a mesma.

sub string

Ao acessar uma string usando reflexão, você obtém o ponteiro real:

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

Portanto, alterar para isso alterará a string que contém um ponteiro para ela, mas como s3é criada com uma nova string devido a substring()ela, não mudará.

mudança


Isso funciona apenas para literais e é uma otimização em tempo de compilação.
SpacePrez

2
@ Zaphod42 Não é verdade. Você também pode chamar internmanualmente em uma String não literal e colher os benefícios.
Chris Hayes

Observe, porém: você deseja usar interncriteriosamente. Internar tudo não ganha muito e pode ser a fonte de alguns momentos de coçar a cabeça quando você adiciona reflexão à mistura.
cHao 24/01

Test1e Test1são inconsistentes test1==test2e não seguem as convenções de nomenclatura java.
C0der

50

Você está usando a reflexão para contornar a imutabilidade de String - é uma forma de "ataque".

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

Existem casos de uso em que esse tipo de código pode ser usado para sua vantagem e ter "boa codificação", como limpar senhas da memória no momento mais rápido possível (antes do GC) .

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


30

Você está usando a reflexão para acessar os "detalhes da implementação" do objeto string. Imutabilidade é o recurso da interface pública de um objeto.


24

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

O segundo efeito que você vê é que tudo Stringmuda, 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 seqüência de caracteres com o mesmo valor serão realmente o mesmo objeto. Quando você cria uma String comnew ela, ela não será internada automaticamente e você não verá esse efeito.

#substringaté recentemente (Java 7u6) funcionava de maneira semelhante, o que explicaria o comportamento na versão original da sua pergunta. Ele não criou uma nova matriz de caracteres de backup, mas reutilizou a da String original; acabou de criar um novo objeto String que usava um deslocamento e um comprimento para apresentar apenas uma parte dessa matriz. Isso geralmente funcionava como Strings são imutáveis ​​- a menos que você contorne isso. Esta propriedade de#substring também significava que toda a String original não podia ser coletada como lixo quando ainda existia uma substring mais curta criada a partir dela.

No Java atual e na sua versão atual da pergunta, não há comportamento estranho de #substring.


2
Na verdade, os modificadores de visibilidade são (ou pelo menos foram) destinados a proteger contra códigos maliciosos - no entanto, você precisa definir um SecurityManager (System.setSecurityManager ()) para ativar a proteção. Como garantir isso realmente é uma outra questão ...
sleske

2
Merece uma votação antecipada porque você enfatiza que os modificadores de acesso não se destinam a 'proteger' o código. Isso parece ser amplamente mal compreendido em Java e .NET. Embora o comentário anterior contradiga isso; Não sei muito sobre Java, mas no .NET isso certamente é verdade. Em nenhum dos idiomas os usuários devem assumir que isso torna seu código à prova de hackers.
Tom W

Não é possível violar o contrato finalnem mesmo através da reflexão. Além disso, como mencionado em outra resposta, desde o Java 7u6, #substringnão compartilha matrizes.
Ntskrnl

Na verdade, o comportamento de finalmudou ao longo do tempo ...: -O De acordo com a palestra "Reflection Madness" de Heinz que publiquei no outro segmento, finalsignificava final no JDK 1.1, 1.3 e 1.4, mas poderia ser modificado usando a reflexão usando sempre o 1.2 e em 1,5 e 6 na maioria dos casos ...
haraldK

11
finalos campos podem ser alterados através do nativecódigo, conforme feito pela estrutura de serialização ao ler os campos de uma instância serializada, bem como System.setOut(…)que modifica a System.outvariável final . O último é o recurso mais interessante, pois a reflexão com substituição de acesso não pode alterar os static finalcampos.
Holger

11

A imutabilidade de strings é da perspectiva da interface. Você está usando a reflexão para ignorar a interface e modificar diretamente os internos das instâncias de String.

s1e s2são alterados porque são atribuídos à mesma instância String "interna". Você pode descobrir um pouco mais sobre essa parte deste artigo sobre igualdade e internamento de strings. Você pode se surpreender ao descobrir que, no seu código de amostra, s1 == s2retorna true!


10

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

Citando a representação interna de string do Oracle Tunes Java :

No novo paradigma, os campos de deslocamento e contagem de String foram removidos, portanto, as substrings não compartilham mais o valor char [] subjacente.

Com essa alteração, isso pode ocorrer sem reflexão (???).


2
Se o OP estivesse usando um Sun / Oracle JRE mais antigo, a última instrução imprimiria "Java!" (como ele acidentalmente postou). Isso afeta apenas o compartilhamento da matriz de valores entre cadeias e sub-cadeias. Você ainda não pode alterar o valor sem truques, como reflexão.
haraldK

7

Há realmente duas perguntas aqui:

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

Ponto 1: Exceto para a ROM, não há memória imutável no seu computador. Hoje em dia até a ROM às vezes é gravável. Sempre há algum código em algum lugar (seja o kernel ou o código nativo que contorna o ambiente gerenciado) que pode gravar no seu endereço de memória. Então, na "realidade", não, eles não são absolutamente imutáveis.

Ponto 2: Isso ocorre porque a substring provavelmente está alocando uma nova instância de string, que provavelmente está copiando a matriz. É possível implementar substring de forma que não faça uma cópia, mas isso não significa que sim. Existem tradeoffs envolvidos.

Por exemplo, manter uma referência para reallyLargeString.substring(reallyLargeString.length - 2)fazer com que uma grande quantidade de memória seja mantida ativa ou apenas alguns bytes?

Isso depende de como a substring é implementada. Uma cópia profunda manterá menos memória ativa, mas ficará um pouco mais lenta. Uma cópia superficial manterá mais memória ativa, mas será mais rápida. O uso de uma cópia profunda também pode reduzir a fragmentação de heap, pois o objeto de seqüência de caracteres e seu buffer podem ser alocados em um bloco, em oposição a 2 alocações de heap separadas.

De qualquer forma, parece que sua JVM optou por usar cópias profundas para chamadas de substring.


3
A ROM real é tão imutável quanto uma impressão fotográfica envolto em plástico. O padrão é definido permanentemente quando a bolacha (ou impressão) é desenvolvida quimicamente. Memórias eletricamente alteráveis, incluindo chips de RAM , podem se comportar como ROM "verdadeira" se os sinais de controle necessários para escrevê-la não puderem ser energizados sem a adição de conexões elétricas adicionais ao circuito em que está instalado. Na verdade, não é incomum que os dispositivos incorporados incluam RAM configurada na fábrica e mantida por uma bateria de reserva e cujo conteúdo precisaria ser recarregado pela fábrica se a bateria falhar.
supercat

3
@ supercat: Seu computador não é um desses sistemas embarcados. :) As verdadeiras ROMs com fio não são comuns nos PCs há uma década ou duas; tudo está EEPROM e flash hoje em dia. Basicamente, todo endereço visível ao usuário que se refere à memória se refere à memória potencialmente gravável.
cHao 24/01

@cHao: Muitos chips flash permitem que partes sejam protegidas contra gravação de uma maneira que, se puder ser desfeita, exigiria a aplicação de tensões diferentes das necessárias para a operação normal (que as placas-mãe não seriam equipadas para fazer). Eu esperaria que as placas-mãe usassem esse recurso. Além disso, não tenho certeza sobre os computadores de hoje, mas historicamente alguns computadores tiveram uma região de RAM que foi protegida contra gravação durante o estágio de inicialização e só pôde ser desprotegida por uma redefinição (o que forçaria a execução a iniciar a partir da ROM).
Supercat

2
@ supercat Eu acho que você está perdendo o objetivo, que é que as strings, armazenadas na RAM, nunca serão realmente imutáveis.
Scott Wisniewski

5

Para adicionar à resposta do @ haraldK - este é um truque 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 a cadeia é declarada como umString s = "Hello World"; , ela está sendo inserida em um pool de objetos especial para potencial reutilização. O problema é que o compilador colocará uma referência à versão modificada no momento da compilação e, uma vez que o usuário modifique a sequência 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 erro a seguir:

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

Irá imprimir:

Hello Java!

Houve outro problema que experimentei quando estava implementando uma computação pesada sobre essas seqüências de risco. Houve um erro que ocorreu em 1 em 1000000 vezes durante o cálculo 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.


Pode ter sido um problema de segurança de encadeamento mascarado pelo tempo de execução mais lento e menos simultaneidade sem o JIT.
precisa saber é o seguinte

@TedPennings Da minha descrição, eu simplesmente não queria entrar muito nos detalhes. Na verdade, passei alguns dias tentando localizá-lo. Foi um algoritmo de thread único que calculou a distância entre dois textos escritos em dois idiomas diferentes. Encontrei duas correções possíveis para o problema - uma era desligar o JIT e a segunda era adicionar literalmente não operação String.format("")dentro de um dos loops internos. Há uma chance de ser um problema que não seja o JIT, mas acredito que foi o JIT, porque esse problema nunca foi reproduzido novamente após a adição deste no-op.
Andrey Chaschev

Eu estava fazendo isso com uma versão inicial do JDK ~ 7u9, para que pudesse ser.
Andrey Chaschev

11
@Andrey Chaschev: “Encontrei duas correções possíveis para o problema”… a terceira correção possível, para não invadir os Stringinternos, não veio à sua mente?
Holger

11
@ Penned Pennings: questões de segurança de tópicos e questões de JIT geralmente são as mesmas. É permitido ao JIT gerar código que se baseie nas finalgarantias de segurança do encadeamento de campo que quebram ao modificar os dados após a construção do objeto. Assim, você pode vê-lo como um problema JIT ou MT, como quiser. O verdadeiro problema é invadir Stringe modificar os dados que se espera sejam imutáveis.
Holger

5

De acordo com o conceito de pool, todas as variáveis ​​String que contêm o mesmo valor apontarão para o mesmo endereço de memória. Portanto, s1 e s2, ambos contendo o mesmo valor de "Hello World", apontam para o mesmo local de memória (por exemplo, M1).

Por outro lado, o s3 contém "Mundo", portanto apontará para uma alocação de memória 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 memória M1 apontado por s1 e s2 foi alterado.

Portanto, como resultado, o local da memória M1 foi modificado, causando alterações no valor de s1 e s2.

Mas o valor do local M2 permanece inalterado, portanto, s3 contém o mesmo valor original.


5

O motivo pelo qual o s3 realmente não muda é porque, em Java, quando você faz uma substring, a matriz de caracteres de valor de uma substring é copiada internamente (usando Arrays.copyOfRange ()).

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


2
Como essa resposta adicionou algo às respostas diante de você?
Grey

Observe também que esse é um comportamento completamente novo e não é garantido por nenhuma especificação.
Paŭlo Ebermann

A implementação de String.substring(int, int)mudou com o Java 7u6. Antes 7u6, a JVM seria apenas manter um ponteiro para o original Stringé char[]em conjunto com um índice e comprimento. Após 7u6, ele copia a substring para um novo recurso. StringHá prós e contras.
Eric Jablow

2

String é imutável, mas, através da reflexão, você pode alterar a classe String. Você acabou de redefinir a classe String como mutável em tempo real. Você pode redefinir os métodos para serem públicos, privados ou estáticos, se desejar.


2
Se você alterar a visibilidade dos campos / métodos que não é útil porque em tempo de compilação que são privados
Bohemian

11
Você pode alterar a acessibilidade nos métodos, mas não pode alterar o status público / privado e não pode torná-los estáticos.
Grey

1

[Isenção de responsabilidade, este é um estilo de resposta deliberadamente opinativo, pois sinto que é mais necessária uma resposta "não faça isso em crianças em casa"

O pecado é a linha field.setAccessible(true); que diz violar a API pública, permitindo o acesso a um campo privado. Isso é um enorme buraco de segurança que pode ser bloqueado através da configuração de um gerenciador de segurança.

O fenômeno na pergunta são detalhes de implementação que você nunca veria ao não usar essa perigosa linha de código para violar os modificadores de acesso via reflexão. Claramente, duas (normalmente) seqüências imutáveis ​​podem compartilhar a mesma matriz de caracteres. Se uma substring compartilha a mesma matriz, depende se é possível e se o desenvolvedor pensou em compartilhá-la. Normalmente, esses são detalhes de implementação invisíveis que você não precisa saber, a menos que grave o modificador de acesso na cabeça com essa linha de código.

Simplesmente não é uma boa ideia confiar nesses detalhes que não podem ser experimentados sem violar os modificadores de acesso usando reflexão. O proprietário dessa classe suporta apenas a API pública normal e é livre para fazer alterações na implementação no futuro.

Dito tudo isso, a linha de código é realmente muito útil quando você tem uma arma segurando sua cabeça, forçando-o a fazer coisas perigosas. Usar essa porta traseira geralmente é um cheiro de código que você precisa atualizar para um código de biblioteca melhor, onde você não precisa pecar. Outro uso comum dessa perigosa linha de código é escrever uma "estrutura de vodu" (orm, recipiente de injeção, ...). Muitas pessoas se interessam por essas estruturas (a favor e contra elas), então evitarei convidar uma guerra de chamas dizendo nada além da grande maioria dos programadores que não precisam ir para lá.


1

As strings são criadas na área permanente da memória heap da JVM. Então, sim, é realmente imutável e não pode ser alterado após a criação. Como na JVM, existem três tipos de memória heap: 1. Geração jovem 2. Geração antiga 3. Geração permanente.

Quando qualquer objeto é criado, ele entra na área de heap da geração jovem e na área de PermGen reservada para o pool de String.

Aqui estão mais detalhes que você pode obter e obter mais informações em: Como a Coleta de Lixo funciona em Java .


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.