2015-09-17 11 views
82

Java pool di stringa accoppiato con la riflessione in grado di produrre qualche risultato inimmaginabile in Java:Come funziona lo snippet di codice Java? (Piscina String e riflessione)

import java.lang.reflect.Field; 

class MessingWithString { 
    public static void main (String[] args) { 
     String str = "Mario"; 
     toLuigi(str); 
     System.out.println(str + " " + "Mario"); 
    } 

    public static void toLuigi(String original) { 
     try { 
      Field stringValue = String.class.getDeclaredField("value"); 
      stringValue.setAccessible(true); 
      stringValue.set(original, "Luigi".toCharArray()); 
     } catch (Exception ex) { 
      // Ignore exceptions 
     } 
    } 
} 

Sopra codice stamperà:

"Luigi Luigi" 

Che cosa è successo a Mario?

+7

@Joe direi lascia passare. [Jeff Atwood: "Ho imparato a smettere di preoccuparmi e ad amare (alcuni) la duplicazione. E dovresti farlo anche tu."] (Https://blog.stackoverflow.com/2010/11/dr-strangedupe-or-how-i- doppione appresa-smettere-preoccuparsi-e-amore /) – Mindwin

+3

@Mindwin: Ciò non significa che dovremmo smettere di chiudere le domande come duplicate se lo sono davvero. In effetti, l'articolo di Jeff * incoraggia * noi a chiudere le domande come duplicati - perché questo è il modo di collegarli. –

risposta

94

Cosa è successo a Mario ??

L'hai cambiato, in pratica. Sì, con la riflessione si può violare l'immutabilità delle stringhe ... e grazie all'internamento delle stringhe, ciò significa che qualsiasi utilizzo di "Mario" (diverso da un'espressione costante di stringa più grande, che sarebbe stata risolta in fase di compilazione) terminerà come "Luigi" nel resto del programma.

Questo i generi di cosa è il motivo di riflessione richiede autorizzazioni di protezione ...

Si noti che l'espressione str + " " + "Mario" fa non eseguire una concatenazione in fase di compilazione, a causa della sinistra-associatività di +. È efficacemente (str + " ") + "Mario", motivo per cui è ancora possibile vedere Luigi Luigi. Se si modifica il codice per:

System.out.println(str + (" " + "Mario")); 

... poi vedrete Luigi Mario come il compilatore ha internato " Mario" in una stringa diversa da "Mario".

+0

Il bit "altro che in un'espressione costante di stringa più grande" potrebbe non essere vero al 100%, sempre. Nella domanda, il 'System.out.println' call usa un'espressione costante in fase di compilazione ('" "+" Mario "'), eppure quell'istanza di "Mario" continua a essere modificata. Sospetto che ciò sia dovuto a un'ottimizzazione in base alla quale "Mario" è internato e "Mario" si riferisce allo stesso spazio di memoria a causa del fatto che si tratta di una corrispondenza di suffisso, sebbene non l'abbia confermata. Un caso interessante per ciò che è una affermazione generalmente vera, però. (Oppure sto solo interpretando male se si tratta di una costante in fase di compilazione.) –

+7

@ChrisHayes: No, non è un'espressione costante in fase di compilazione a causa dell'associatività di '+'. Viene valutato come '(str +" ") +" Mario "'. Se si stampa solo "" "+ Mario' o" "" + "Mario" + str' * allora * si ha la concatenazione in fase di compilazione, e si ottiene comunque 'Mario' nell'output. –

+0

Ah, capisco. Questo ha senso, se non è immediatamente intuitivo. Grazie per la spiegazione. –

24

Era impostato su Luigi. Le stringhe in Java sono immutabili; in tal modo, il compilatore può interpretare tutte le menzioni di "Mario" come riferimenti allo stesso oggetto di pool di costanti String (approssimativamente, "posizione di memoria"). Hai usato il riflesso per cambiare quell'elemento; quindi tutto il "Mario" nel tuo codice ora è come se scrivessi "Luigi".

+1

* "... come riferimenti alla stessa posizione di memoria ..." * - Il compilatore non occupa posizioni di memoria e il sistema di runtime può Non farlo perché la posizione di memoria di qualsiasi stringa può essere modificata in qualsiasi momento dal garbage collector. (Capisco cosa stai cercando di dire ... ma lo stai esprimendo in modo errato.Se stavi parlando di C o C++, questa spiegazione è approssimativamente corretta.Per Java non lo è.) –

+0

@StephenC: Mentre avrebbe è stato meglio dire "stesso indice nel pool costante di String", alla fine l'effetto è identico: "Mario" '* è * archiviato in una posizione di memoria (perché anche JVM deve essere interpretato sull'architettura sottostante, dove sarà assegnato da qualche parte), e se gc lo muove, rimane comunque vero che tutte le menzioni di "Mario" si riferiranno alla stessa posizione (spostata). Comunque, tu hai un punto: dovrei usare il gergo appropriato per Java, quindi lo cambierò. – Amadan

+1

Il modo migliore per dire è dire che sono tutti lo stesso oggetto. Ed è in definitiva il sistema di runtime Java che garantisce questo non il compilatore. –

9

I valori letterali stringa vengono memorizzati nel pool di stringhe e viene utilizzato il loro valore canonico. Entrambi i letterali "Mario" non sono solo stringhe con lo stesso valore, sono lo stesso oggetto. Manipolare uno di essi (usando la riflessione) modificherà "entrambi", poiché sono solo due riferimenti allo stesso oggetto.

16

Per spiegare le risposte esistenti un po 'di più, diamo un'occhiata al codice byte generato (solo il metodo main() qui).

Byte Code

Ora, le eventuali modifiche al contenuto della posizione di quel interesserà sia i riferimenti (e qualsiasi altro dare troppo).

8

È solo cambiato il String di String costante piscinaMario-Luigi che è stato riferito da più String s, quindi ogni riferimento a letteraleMario è ora Luigi.

Field stringValue = String.class.getDeclaredField("value"); 

Hai recuperato il campo char[] denominato value dalla classe String

stringValue.setAccessible(true); 

renderlo accessibile.

stringValue.set(original, "Luigi".toCharArray()); 

hai cambiato originalString campo per Luigi. Ma l'originale èil Stringletterale e letterale appartiene alla piscina String e tutti sono internato. Il che significa che tutti i letterali con lo stesso contenuto si riferiscono allo stesso indirizzo di memoria.

String a = "Mario";//Created in String pool 
String b = "Mario";//Refers to the same Mario of String pool 
a == b//TRUE 
//You changed 'a' to Luigi and 'b' don't know that 
//'a' has been internally changed and 
//'b' still refers to the same address. 

In pratica si hanno cambiato il Mario di String piscina che ha ottenuto riflette in tutti i settori di riferimento. Se crei StringObject (ad esempio new String("Mario")) anziché letterale, non dovrai affrontare questo comportamento perché avrai due diversi Mario s.

5

Le altre risposte spiegano adeguatamente cosa sta succedendo. Volevo solo aggiungere il punto che funziona solo se non è installato security manager. Quando si esegue il codice dalla riga di comando per impostazione predefinita non lo è, e si possono fare cose come questa. Tuttavia, in un ambiente in cui il codice attendibile è mischiato a un codice non affidabile, come un server applicazioni in un ambiente di produzione o una sandbox dell'applet in un browser, in genere è presente un gestore della sicurezza e non ti sarà consentito questo tipo di shenanigans, quindi questo è meno di un buco di sicurezza terribile come sembra.

3

Un altro punto correlato: è possibile utilizzare il pool costante per migliorare le prestazioni dei confronti tra stringhe in alcune circostanze, utilizzando il metodo String.intern().

Questo metodo restituisce l'istanza di String con lo stesso contenuto della stringa su cui è invocata dal pool di costanti String, aggiungendola se non è ancora presente. In altre parole, dopo aver usato intern(), tutte le stringhe con gli stessi contenuti sono garantite per essere la stessa istanza di String l'una rispetto all'altra e come tutte le costanti String con tali contenuti, ovvero è possibile utilizzare l'operatore di uguale (==).

Questo è solo un esempio, che non è molto utile per sé, ma illustra il punto:

class Key { 
    Key(String keyComponent) { 
     this.keyComponent = keyComponent.intern(); 
    } 

    public boolean equals(Object o) { 
     // String comparison using the equals operator allowed due to the 
     // intern() in the constructor, which guarantees that all values 
     // of keyComponent with the same content will refer to the same 
     // instance of String: 
     return (o instanceof Key) && (keyComponent == ((Key) o).keyComponent); 
    } 

    public int hashCode() { 
     return keyComponent.hashCode(); 
    } 

    boolean isSpecialCase() { 
     // String comparison using equals operator valid due to use of 
     // intern() in constructor, which guarantees that any keyComponent 
     // with the same contents as the SPECIAL_CASE constant will 
     // refer to the same instance of String: 
     return keyComponent == SPECIAL_CASE; 
    } 

    private final String keyComponent; 

    private static final String SPECIAL_CASE = "SpecialCase"; 
} 

Questo piccolo trucco non vale la pena progettare il vostro codice intorno, ma vale la pena tenere a mente per il giorno in cui si nota un po 'più di velocità potrebbe essere sfruttato da un po' di codice sensibile alle prestazioni utilizzando l'operatore == su una stringa con un uso giudizioso di intern().

Problemi correlati