2015-09-17 14 views
82

Java-String-Pool mit Reflexion gekoppelt kann einige unvorstellbar Ergebnis in Java produzieren:Wie funktioniert dieses Java Code-Snippet? (String-Pool und Reflexion)

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 
     } 
    } 
} 

Above Code drucken:

"Luigi Luigi" 

Was Mario passiert?

+7

@Joe Ich würde sagen, lassen Sie es passieren. [Jeff Atwood: "Ich habe gelernt, aufzuhören, sich Sorgen zu machen und Duplikate zu lieben. Und das solltest du auch."] (Https://blog.stackoverflow.com/2010/11/dr-strangedupe- oder-how-i- erlernt-zu-stoppen-sorgen-und-liebes-duplikation /) – Mindwin

+3

@Mindwin: Es bedeutet nicht, dass wir aufhören sollten, Fragen als Duplikate zu beenden, wenn sie wirklich so sind. In der Tat ermutigt uns Jeffs Artikel * dazu, Fragen als Duplikate zu schließen - weil das die Art ist, sie zu verlinken. –

Antwort

94

Was ist mit Mario passiert?

Sie haben es grundlegend geändert. Ja, mit Reflektion kann man die Unveränderlichkeit von Strings verletzen ... und aufgrund des String-Internings bedeutet dies, dass jede Verwendung von "Mario" (anders als in einem größeren String-Konstantenausdruck, der zur Kompilierungszeit aufgelöst worden wäre) endet als "Luigi" im Rest des Programms.

Diese Art der Sache ist, warum Reflexion Sicherheitsberechtigungen erfordert ...

Beachten Sie, dass der Ausdruck str + " " + "Mario" tut nicht jede Kompilierung-Verkettung durchführen, aufgrund der links Assoziativität von +. Es ist effektiv (str + " ") + "Mario", weshalb Sie immer noch Luigi Luigi sehen. Wenn Sie den Code ändern:

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

... dann werden Sie sehen, Luigi Mario als der Compiler interniert haben, werden " Mario" in eine andere Zeichenfolge zu "Mario".

+0

Das Bit "anders als in einem größeren Zeichenfolgenkonstantenausdruck" ist möglicherweise die ganze Zeit nicht 100% wahr. In der Frage der 'System.out.println' call verwendet einen kompilierzeitkonstanten Ausdruck ('" "+" Mario ""), aber diese Instanz von "Mario" wird immer noch geändert. Ich vermute, dass dies auf eine Optimierung zurückzuführen ist, bei der "Mario" interniert ist und "Mario" aufgrund einer Suffix-Übereinstimmung auf den gleichen Speicherplatz verweist, obwohl ich dies nicht bestätigt habe. Ein interessanter Randfall für eine allgemein wahrheitsgetreue Aussage. (Oder ich fehlinterpretiere nur, ob das eine Kompilierzeitkonstante ist.) –

+7

@ChrisHayes: Nein, das ist kein kompilierbarer Konstantenausdruck aufgrund der Assoziativität von '+'. Es wird als "(str +" ") +" Mario "" bewertet. Wenn Sie nur "" "+" Mario "oder" "" + "Mario" + "str" ​​* drucken, dann * haben Sie eine Kompilierungs-Verkettung, und Sie erhalten immer noch "Mario" in der Ausgabe. –

+0

Ah, ich verstehe. Das macht Sinn, wenn es nicht sofort intuitiv ist. Danke für die Erklärung. –

24

Es war Luigi gesetzt. Strings in Java sind unveränderlich; Daher kann der Compiler alle Erwähnungen von "Mario" als Verweise auf das gleiche String-Konstanten-Pool-Element (ungefähr "Speicherort") interpretieren. Du hast Reflexion benutzt, um diesen Gegenstand zu ändern. so alle "Mario" in Ihrem Code sind jetzt, als ob Sie "Luigi" geschrieben haben.

+1

* "... als Verweise auf denselben Speicherort ..." * - Der Compiler behandelt keine Speicherorte und das Laufzeitsystem kann Tun Sie dies nicht, da der Speicherort eines beliebigen String jederzeit durch den Garbage Collector geändert werden kann. (Ich verstehe, was Sie zu sagen versuchen ... aber Sie drücken es falsch aus. Wenn Sie über C oder C++ sprachen, ist diese Erklärung in etwa richtig. Für Java ist es nicht.) –

+0

@StephenC: Während es hätte Besser wäre es, "denselben Index im String-Konstanten-Pool" zu sagen, am Ende ist der Effekt identisch: "" Mario "* * ist * in einem Speicherort gespeichert (weil selbst JVM irgendwann auf der zugrunde liegenden Architektur interpretiert werden muss, wo es wird irgendwo zugewiesen), und wenn gc es bewegt, bleibt es immer wahr, dass alle Erwähnungen von "Mario" sich auf denselben (verschobenen) Ort beziehen. Dennoch, Sie haben einen Punkt - ich sollte Java-geeigneten Jargon verwenden, also werde ich es ändern. – Amadan

+1

Der beste Weg zu sagen ist zu sagen, dass sie alle das gleiche Objekt sind. Und es ist letztendlich das Java-Laufzeitsystem, das dies nicht den Compiler gewährleistet. –

9

String-Literale werden im String-Pool gespeichert und ihr kanonischer Wert wird verwendet. Beide "Mario" Literale sind nicht nur Strings mit dem gleichen Wert, sie sind das gleiche Objekt. Wenn Sie eine davon manipulieren (indem Sie die Reflektion verwenden), ändern Sie "beide", da es sich nur um zwei Referenzen auf dasselbe Objekt handelt.

16

Um die vorhandenen Antworten ein wenig mehr zu erklären, werfen wir einen Blick auf Ihren generierten Byte-Code (hier nur die Methode main()).

Byte Code

nun Änderungen an den Inhalt der von dieser Stelle wirkt sich sowohl die Referenzen (und alle anderen Sie geben auch).

8

Sie verändert nur die String von Zeichenkette PoolMario-Luigi, die von mehreren String s verwiesen wurde, so ist jede Bezugnahme auf wörtlicheMario jetzt Luigi.

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

Sie haben das von der Klasse value Feld mit dem Namen char[] geholt String

stringValue.setAccessible(true); 

zugänglich machen.

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

Sie geändert originalString Feld Luigi. Aber original ist Mario die Stringliteral und literal gehört zum String Pool und alle sind intern. Das bedeutet, dass alle Literale, die denselben Inhalt haben, auf die gleiche Speicheradresse verweisen.

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. 

Grundsätzlich haben Sie den Mario von String Pool geändert, die in allen Referenzierung Felder reflektiert bekam. Wenn Sie StringObject (d. H. new String("Mario")) anstelle von Literal erstellen, werden Sie nicht auf dieses Verhalten stoßen, da Sie dann zwei verschiedene Mario s haben.

5

Die anderen Antworten erklären ausreichend, was vor sich geht. Ich wollte nur hinzufügen, dass dies nur funktioniert, wenn keine security manager installiert ist. Wenn Code von der Befehlszeile standardmäßig ausgeführt wird, gibt es das nicht, und Sie können solche Dinge tun. In einer Umgebung, in der vertrauenswürdiger Code mit nicht vertrauenswürdigem Code gemischt wird, z. B. ein Anwendungsserver in einer Produktionsumgebung oder eine Applet-Sandbox in einem Browser, ist jedoch in der Regel ein Sicherheitsmanager vorhanden und diese Art von Spielereien sind nicht zulässig Das ist weniger ein schreckliches Sicherheitsloch, wie es scheint.

3

Ein weiterer verwandter Punkt: Sie können den Konstantenpool verwenden, um unter bestimmten Umständen die Leistung von Zeichenfolgenvergleichen zu verbessern, indem Sie die String.intern()-Methode verwenden.

Diese Methode gibt die Instanz von String mit dem gleichen Inhalt zurück wie die Zeichenfolge, aus der sie vom String-Konstantenpool aufgerufen wird, und fügt sie hinzu, wenn sie noch nicht vorhanden ist. Mit anderen Worten, nach der Verwendung von intern() sind alle Zeichenketten mit demselben Inhalt garantiert die gleiche Zeichenketteninstanz und jede String-Konstanten mit diesen Inhalten, was bedeutet, dass Sie dann den Gleichheitsoperator (==) verwenden können.

Dies ist nur ein Beispiel, das für sich allein ist nicht sehr nützlich, aber es zeigt den Punkt:

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"; 
} 

Dieser kleine Trick Code entwerfen um nicht wert ist, aber es lohnt sich wenn man bedenkt, Für den Tag, an dem Sie etwas mehr Geschwindigkeit bemerken, könnte aus etwas leistungsfähigem Code mit Hilfe des == Operators auf einer Schnur mit kluger Verwendung intern() gekökelt werden.