Una stringa Java è davvero immutabile?


399

Sappiamo tutti che Stringè immutabile in Java, ma controlla il seguente codice:

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  

Perché questo programma funziona in questo modo? E perché il valore di s1e s2cambiato, ma non s3?


394
Puoi fare tutti i tipi di stupidi trucchi con la riflessione. Ma fondamentalmente stai rompendo l'adesivo "annullamento della garanzia se rimosso" sulla classe nel momento in cui lo fai.
cHao,

16
@DarshanPatel usa un SecurityManager per disabilitare la riflessione
Sean Patrick Floyd,

39
Se vuoi davvero fare confusione con le cose puoi farlo in modo (Integer)1+(Integer)2=42da fare confusione con l'autoboxing memorizzato nella cache; (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )
Richard Tingle,

15
Potresti essere divertito da questa risposta che ho scritto quasi 5 anni fa stackoverflow.com/a/1232332/27423 - si tratta di elenchi immutabili in C # ma è sostanzialmente la stessa cosa: come posso impedire agli utenti di modificare i miei dati? E la risposta è che non puoi; la riflessione lo rende molto semplice. Un linguaggio tradizionale che non ha questo problema è JavaScript, in quanto non ha un sistema di riflessione che può accedere alle variabili locali all'interno di una chiusura, quindi privato significa davvero privato (anche se non c'è una parola chiave per questo!)
Daniel Earwicker

49
Qualcuno sta leggendo la domanda fino alla fine ?? La domanda è, fammi ripetere: "Perché questo programma funziona in questo modo? Perché il valore di s1 e s2 è cambiato e non è cambiato per s3?" La domanda NON è: perché s1 e s2 sono cambiati! La domanda è: PERCHÉ s3 non è cambiato?
Roland Pihlakas,

Risposte:


403

String è immutabile * ma ciò significa solo che non è possibile modificarlo utilizzando la sua API pubblica.

Quello che stai facendo qui è aggirare l'API normale, usando la riflessione. Allo stesso modo, è possibile modificare i valori degli enum, modificare la tabella di ricerca utilizzata nell'autoboxing intero, ecc.

Ora, la ragione s1e il s2valore di modifica, è che entrambi si riferiscono alla stessa stringa internata. Il compilatore fa questo (come indicato da altre risposte).

Il motivo s3non è stato in realtà un po 'sorprendente per me, poiché pensavo che avrebbe condiviso l' valuearray ( lo faceva nella versione precedente di Java , prima di Java 7u6). Tuttavia, osservando il codice sorgente di String, possiamo vedere che l' valuearray di caratteri per una sottostringa viene effettivamente copiato (usando Arrays.copyOfRange(..)). Questo è il motivo per cui rimane invariato.

È possibile installare un SecurityManager, per evitare che codice dannoso esegua tali operazioni. Ma tieni presente che alcune librerie dipendono dall'uso di questo tipo di trucchi di riflessione (in genere strumenti ORM, librerie AOP ecc.).

*) Inizialmente ho scritto che Stringnon sono veramente immutabili, solo "efficaci immutabili". Ciò potrebbe essere fuorviante nell'attuale implementazione di String, dove l' valuearray è effettivamente contrassegnato private final. Vale comunque la pena notare che non c'è modo di dichiarare immutabile un array in Java, quindi bisogna fare attenzione a non esporlo al di fuori della sua classe, anche con i modificatori di accesso appropriati.


Dato che questo argomento sembra estremamente popolare, ecco alcuni suggerimenti suggeriti da leggere ulteriormente: il discorso Reflection Madness di Heinz Kabutz da JavaZone 2009, che copre molti problemi dell'OP, insieme ad altre riflessioni ... beh ... follia.

Copre il motivo per cui a volte questo è utile. E perché, il più delle volte, dovresti evitarlo. :-)


7
In realtà, Stringinterning fa parte del JLS ( "un letterale stringa si riferisce sempre alla stessa istanza della classe String" ). Ma sono d'accordo, non è buona norma contare sui dettagli di implementazione della Stringclasse.
HaraldK,

3
Forse il motivo per cui le substringcopie invece di utilizzare una "sezione" dell'array esistente, è altrimenti se avessi una stringa enorme se tirassi fuori una piccola sottostringa chiamata tda essa, e in seguito abbandonassi sma mantenessi t, quindi l'enorme array rimarrebbe in vita (non immondizia raccolta). Quindi forse è più naturale che ogni valore di stringa abbia il proprio array associato?
Jeppe Stig Nielsen,

10
La condivisione di matrici tra una stringa e le sue sottostringhe implicava anche che ogni String istanza dovesse contenere variabili per ricordare l'offset nell'array e nella lunghezza indicati. Questo è un sovraccarico da non ignorare dato il numero totale di stringhe e il rapporto tipico tra stringhe normali e sottostringhe in un'applicazione. Dato che dovevano essere valutati per ogni operazione di stringa, ciò significava rallentare ogni operazione di stringa solo a beneficio di una sola operazione, una sottostringa economica.
Holger,

2
@Holger - Sì, la mia comprensione è che il campo offset è stato eliminato nelle recenti JVM. E anche quando era presente non veniva usato così spesso.
Hot Licks

2
@supercat: non importa se si dispone di codice nativo o meno, con implementazioni diverse per stringhe e sottostringa all'interno della stessa JVM o byte[]stringhe per stringhe ASCII e char[]per altri implica che ogni operazione deve controllare che tipo di stringa è prima operativo. Ciò ostacola l'inserimento del codice nei metodi mediante le stringhe, che è il primo passo di ulteriori ottimizzazioni utilizzando le informazioni di contesto del chiamante. Questo è un grande impatto.
Holger

93

In Java, se due variabili primitive di stringa sono inizializzate sullo stesso valore letterale, assegna lo stesso riferimento a entrambe le variabili:

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

inizializzazione

Questo è il motivo per cui il confronto ritorna vero. La terza stringa viene creata utilizzando il substring()quale crea una nuova stringa anziché puntare alla stessa.

sottostringa

Quando accedi a una stringa usando reflection, ottieni il puntatore effettivo:

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

Quindi la modifica a questo cambierà la stringa che tiene un puntatore ad essa, ma come s3viene creata con una nuova stringa a causa di substring()essa non cambierebbe.

modificare


Questo funziona solo per i letterali ed è un'ottimizzazione in fase di compilazione.
SpacePrez,

2
@ Zaphod42 Non è vero. Puoi anche chiamare internmanualmente su una stringa non letterale e ottenere i vantaggi.
Chris Hayes,

Nota, però: vuoi usare interngiudiziosamente. Internare tutto non ti guadagna molto, e può essere la fonte di alcuni momenti graffianti quando aggiungi riflessione al mix.
cHao,

Test1e Test1sono incompatibili con test1==test2e non seguono le convenzioni di denominazione Java.
c0der,

50

Stai usando la riflessione per aggirare l'immutabilità di String - è una forma di "attacco".

Ci sono molti esempi che puoi creare in questo modo (ad esempio puoi anche creare un'istanza di un Voidoggetto ), ma ciò non significa che String non sia "immutabile".

Ci sono casi d'uso in cui questo tipo di codice può essere usato a tuo vantaggio ed essere "una buona codifica", come cancellare le password dalla memoria il prima possibile (prima di GC) .

A seconda del gestore della sicurezza, potrebbe non essere possibile eseguire il codice.


30

Si sta utilizzando la riflessione per accedere ai "dettagli di implementazione" dell'oggetto stringa. L'immutabilità è la caratteristica dell'interfaccia pubblica di un oggetto.


24

I modificatori di visibilità e il finale (cioè l'immutabilità) non sono una misura rispetto al codice dannoso in Java; sono semplicemente strumenti per proteggere dagli errori e per rendere il codice più mantenibile (uno dei maggiori punti di forza del sistema). Questo è il motivo per cui è possibile accedere ai dettagli dell'implementazione interna come l'array di caratteri di supporto per Strings tramite reflection.

Il secondo effetto che vedi è che tutti Stringcambiano mentre sembra che tu cambi solo s1. È una certa proprietà dei letterali String Java che vengono automaticamente internati, ovvero memorizzati nella cache. Due letterali String con lo stesso valore saranno effettivamente lo stesso oggetto. Quando crei una stringa con newessa non verrà internata automaticamente e non vedrai questo effetto.

#substringfino a poco tempo fa (Java 7u6) funzionava in modo simile, il che avrebbe spiegato il comportamento nella versione originale della tua domanda. Non ha creato un nuovo array di caratteri di supporto ma ha riutilizzato quello della stringa originale; ha appena creato un nuovo oggetto String che utilizzava un offset e una lunghezza per presentare solo una parte di tale array. Questo in genere ha funzionato poiché le stringhe sono immutabili, a meno che non lo si elimini. Questa proprietà #substringsignificava anche che l'intera stringa originale non poteva essere raccolta in modo inutile quando esisteva ancora una sottostringa più corta creata da essa.

A partire dall'attuale Java e dalla versione corrente della domanda non vi è alcun comportamento strano di #substring.


2
In realtà, i modificatori di visibilità sono (o almeno erano) intesi come protezione contro codice dannoso - tuttavia, è necessario impostare un SecurityManager (System.setSecurityManager ()) per attivare la protezione. Quanto è sicura questa è un'altra domanda ...
sleske,

2
Merita un voto perché si sottolinea che i modificatori di accesso non intendono proteggere il codice. Questo sembra essere ampiamente frainteso sia in Java che in .NET. Anche se il commento precedente lo contraddice; Non so molto di Java, ma in .NET questo è certamente vero. In nessuna lingua gli utenti dovrebbero supporre che ciò renda il loro codice a prova di hack.
Tom W,

Non è possibile violare il contratto finalanche attraverso la riflessione. Inoltre, come menzionato in un'altra risposta, dal momento che Java 7u6, #substringnon condivide gli array.
ntoskrnl,

In realtà, il comportamento di finalè cambiato nel tempo ...: -O Secondo il discorso di "Reflection Madness" di Heinz ho pubblicato nell'altro thread, finalsignificava definitivo in JDK 1.1, 1.3 e 1.4, ma poteva essere modificato usando la riflessione usando sempre 1.2 , e in 1.5 e 6 nella maggior parte dei casi ...
haraldK,

1
finali campi possono essere modificati tramite il nativecodice come fatto dal framework di serializzazione durante la lettura dei campi di un'istanza serializzata e System.setOut(…)che modifica la System.outvariabile finale . Quest'ultima è la caratteristica più interessante in quanto la riflessione con override di accesso non può cambiare i static finalcampi.
Holger,

11

L'immutabilità delle stringhe è dal punto di vista dell'interfaccia. Stai usando reflection per bypassare l'interfaccia e modificare direttamente gli interni delle istanze String.

s1e s2sono entrambi cambiati perché sono entrambi assegnati alla stessa istanza String "intern". Puoi scoprire qualcosa in più su quella parte di questo articolo su uguaglianza delle stringhe e interning. Potresti essere sorpreso di scoprire che nel tuo codice di esempio, s1 == s2ritorna true!


10

Quale versione di Java stai usando? Da Java 1.7.0_06, Oracle ha modificato la rappresentazione interna di String, in particolare la sottostringa.

Citando dalla rappresentazione della stringa interna di Oracle Tunes Java :

Nel nuovo paradigma, i campi String offset e count sono stati rimossi, quindi le sottostringhe non condividono più il valore char [] sottostante.

Con questo cambiamento, può accadere senza riflessione (???).


2
Se l'OP utilizzava un JRE Sun / Oracle precedente, l'ultima istruzione avrebbe stampato "Java!" (come ha pubblicato per caso). Ciò influisce solo sulla condivisione dell'array di valori tra stringhe e stringhe secondarie. Non è ancora possibile modificare il valore senza trucchi, come la riflessione.
HaraldK,

7

Ci sono davvero due domande qui:

  1. Le stringhe sono davvero immutabili?
  2. Perché s3 non è cambiato?

Al punto 1: ad eccezione della ROM, non esiste memoria immutabile nel computer. Oggi anche la ROM è talvolta scrivibile. C'è sempre del codice da qualche parte (sia esso il kernel o il codice nativo che elude l'ambiente gestito) che può scrivere nel tuo indirizzo di memoria. Quindi, nella "realtà", no, non sono assolutamente immutabili.

Al punto 2: ciò è dovuto al fatto che la sottostringa sta probabilmente allocando una nuova istanza di stringa, che probabilmente sta copiando l'array. È possibile implementare la sottostringa in modo tale da non farne una copia, ma ciò non significa che lo faccia. Ci sono compromessi coinvolti.

Ad esempio, è necessario mantenere un riferimento per reallyLargeString.substring(reallyLargeString.length - 2)mantenere viva una grande quantità di memoria o solo pochi byte?

Dipende da come viene implementata la sottostringa. Una copia profonda manterrà meno memoria attiva, ma verrà eseguita leggermente più lentamente. Una copia superficiale manterrà viva più memoria, ma sarà più veloce. L'uso di una copia profonda può anche ridurre la frammentazione dell'heap, poiché l'oggetto stringa e il relativo buffer possono essere allocati in un blocco, al contrario di 2 allocazioni di heap separate.

In ogni caso, sembra che la tua JVM abbia scelto di utilizzare copie profonde per le chiamate di sottostringa.


3
La vera ROM è immutabile quanto una stampa fotografica racchiusa in plastica. Il motivo viene impostato in modo permanente quando il wafer (o la stampa) viene sviluppato chimicamente. Le memorie modificabili elettricamente, inclusi i chip RAM , possono comportarsi come ROM "vere" se i segnali di controllo necessari per scrivere non possono essere alimentati senza l'aggiunta di ulteriori collegamenti elettrici al circuito in cui è installato. In realtà non è insolito che i dispositivi embedded includano la RAM impostata in fabbrica e gestita da una batteria di backup e il cui contenuto dovrebbe essere ricaricato dalla fabbrica in caso di guasto della batteria.
supercat

3
@supercat: Tuttavia, il tuo computer non è uno di quei sistemi integrati. :) Le vere ROM cablate non sono comuni nei PC da un decennio o due; tutto è EEPROM e flash in questi giorni. Fondamentalmente ogni indirizzo visibile all'utente che si riferisce alla memoria, si riferisce alla memoria potenzialmente scrivibile.
cHao,

@cHao: molti chip flash consentono alle parti di essere protette da scrittura in un modo che, se potesse essere annullato, richiederebbe l'applicazione di tensioni diverse da quelle richieste per il normale funzionamento (che le schede madri non sarebbero equipaggiate per fare). Mi aspetterei che le schede madri utilizzino quella funzione. Inoltre, non sono sicuro dei computer di oggi, ma storicamente alcuni computer avevano una regione di RAM che era protetta da scrittura durante la fase di avvio e non poteva essere protetta da un reset (che avrebbe costretto l'esecuzione a partire dalla ROM).
supercat

2
@supercat Penso che ti stia perdendo il punto dell'argomento, ovvero che le stringhe, memorizzate nella RAM, non saranno mai veramente immutabili.
Scott Wisniewski,

5

Da aggiungere alla risposta di @ haraldK: si tratta di un hack di sicurezza che potrebbe causare un grave impatto nell'app.

La prima cosa è una modifica a una stringa costante memorizzata in un pool di stringhe. Quando la stringa viene dichiarata come a String s = "Hello World";, viene inserita in un pool di oggetti speciale per un ulteriore potenziale riutilizzo. Il problema è che il compilatore inserirà un riferimento alla versione modificata in fase di compilazione e una volta che l'utente modifica la stringa memorizzata in questo pool in fase di esecuzione, tutti i riferimenti nel codice punteranno alla versione modificata. Ciò comporterebbe un seguente bug:

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

Stampa:

Hello Java!

C'è stato un altro problema che ho riscontrato durante l'implementazione di un calcolo pesante su stringhe così rischiose. Si è verificato un errore in circa 1 su 1000000 volte durante il calcolo che ha reso il risultato indeterminato. Sono stato in grado di trovare il problema spegnendo la JIT - ottenevo sempre lo stesso risultato con la JIT disattivata. La mia ipotesi è che il motivo sia stato questo hack di sicurezza String che ha rotto alcuni dei contratti di ottimizzazione JIT.


Potrebbe essere stato un problema di sicurezza dei thread mascherato da tempi di esecuzione più lenti e meno concorrenza senza JIT.
Ted Pennings,

@TedPennings Dalla mia descrizione potrebbe, non volevo approfondire troppo i dettagli. In realtà ho trascorso un paio di giorni cercando di localizzarlo. Era un algoritmo a thread singolo che calcolava una distanza tra due testi scritti in due lingue diverse. Ho trovato due possibili soluzioni per il problema: una era di spegnere la JIT e la seconda era quella di aggiungere letteralmente no-op String.format("")all'interno di uno dei circuiti interni. Esiste la possibilità che si tratti di un problema diverso dall'errore JIT, ma credo che sia stato JIT, perché questo problema non è mai stato riprodotto dopo l'aggiunta di questo no-op.
Andrey Chaschev,

Lo stavo facendo con una prima versione di JDK ~ 7u9, quindi potrebbe essere.
Andrey Chaschev,

1
@Andrey Chaschev: "Ho trovato due possibili soluzioni per il problema" ... la terza soluzione possibile, per non hackerare gli Stringinterni, non ti è venuta in mente?
Holger

1
@Ted Pennings: problemi di sicurezza dei thread e problemi JIT sono spesso gli stessi. Alla JIT è consentito generare codice che si basa sulle finalgaranzie di sicurezza del thread di campo che si rompono quando si modificano i dati dopo la costruzione dell'oggetto. Quindi puoi vederlo come un problema JIT o un problema MT proprio come preferisci. Il vero problema è hackerare Stringe modificare i dati che dovrebbero essere immutabili.
Holger

5

Secondo il concetto di pooling, tutte le variabili String che contengono lo stesso valore punteranno allo stesso indirizzo di memoria. Pertanto s1 e s2, entrambi contenenti lo stesso valore di "Hello World", indicheranno la stessa posizione di memoria (ad esempio M1).

D'altra parte, s3 contiene "World", quindi indicherà una diversa allocazione di memoria (diciamo M2).

Quindi ora ciò che sta accadendo è che il valore di S1 ​​viene modificato (usando il valore char []). Quindi il valore nella posizione di memoria M1 indicato sia da s1 che da s2 è stato modificato.

Di conseguenza, la posizione di memoria M1 è stata modificata che causa la modifica del valore di s1 e s2.

Ma il valore della posizione M2 rimane invariato, quindi s3 contiene lo stesso valore originale.


5

Il motivo per cui s3 non cambia in realtà è perché in Java quando si esegue una sottostringa l'array di caratteri valore per una sottostringa viene copiato internamente (utilizzando Arrays.copyOfRange ()).

s1 e s2 sono uguali perché in Java si riferiscono entrambi alla stessa stringa internata. È progettato in Java.


2
In che modo questa risposta ha aggiunto qualcosa alle risposte prima di te?
Gray,

Si noti inoltre che si tratta di un comportamento abbastanza nuovo e non garantito da alcuna specifica.
Paŭlo Ebermann,

L'implementazione di è String.substring(int, int)cambiata con Java 7u6. Prima 7u6, la JVM sarebbe solo mantenere un puntatore a quello originale String's char[]insieme a un indice e lunghezza. Dopo 7u6, copia la sottostringa in un nuovo StringCi sono pro e contro.
Eric Jablow,

2

String è immutabile, ma tramite la riflessione puoi modificare la classe String. Hai appena ridefinito la classe String come modificabile in tempo reale. Se lo desideri, puoi ridefinire i metodi in modo che siano pubblici, privati ​​o statici.


2
Se cambi la visibilità di campi / metodi non è utile perché in fase di compilazione sono privati
Boemo

1
Puoi modificare l'accessibilità sui metodi ma non puoi cambiare il loro stato pubblico / privato e non puoi renderli statici.
Gray,

1

[Dichiarazione di non responsabilità si tratta di uno stile di risposta deliberatamente ponderato poiché ritengo che una risposta più "non farlo a casa dei bambini" sia garantita]

Il peccato è la linea field.setAccessible(true);che dice di violare l'api pubblica consentendo l'accesso a un campo privato. Questo è un enorme buco di sicurezza che può essere bloccato configurando un gestore della sicurezza.

Il fenomeno nella domanda sono i dettagli di implementazione che non vedresti mai quando non usi quella pericolosa riga di codice per violare i modificatori di accesso tramite la riflessione. Chiaramente due stringhe (normalmente) immutabili possono condividere lo stesso array di caratteri. Il fatto che una sottostringa condivida lo stesso array dipende dal fatto che possa o meno che lo sviluppatore abbia pensato di condividerlo. Normalmente si tratta di dettagli di implementazione invisibili che non dovresti conoscere se non spari al modificatore di accesso attraverso la testa con quella riga di codice.

Semplicemente non è una buona idea fare affidamento su tali dettagli che non possono essere sperimentati senza violare i modificatori di accesso mediante la riflessione. Il proprietario di quella classe supporta solo la normale API pubblica ed è libero di apportare modifiche all'implementazione in futuro.

Detto questo, la linea di codice è davvero molto utile quando hai una pistola che ti tiene in testa costringendoti a fare cose così pericolose. L'uso di quella backdoor è in genere un odore di codice che è necessario aggiornare a un codice di libreria migliore in cui non è necessario peccare. Un altro uso comune di quella pericolosa riga di codice è scrivere un "framework voodoo" (orm, container per iniezione, ...). Molte persone diventano religiose su tali schemi (sia a favore che contro di loro), quindi eviterò di invitare una guerra di fiamma dicendo che nient'altro che la stragrande maggioranza dei programmatori non deve andare lì.


1

Le stringhe vengono create nell'area permanente della memoria heap JVM. Quindi sì, è davvero immutabile e non può essere modificato dopo essere stato creato. Perché nella JVM esistono tre tipi di memoria heap: 1. Young generation 2. Old generation 3. Generazione permanente.

Quando viene creato un oggetto, viene inserito nell'area heap di generazione giovane e nell'area PermGen riservata al pool di stringhe.

Qui puoi trovare ulteriori dettagli e ottenere maggiori informazioni da: Come funziona Garbage Collection in Java .


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.