Ist eine Java-Zeichenfolge wirklich unveränderbar?

Wir alle wissen, dass String in Java unveränderlich ist, aber überprüfen Sie den folgenden Code:

 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 

Warum funktioniert dieses Programm so? Und warum wird der Wert von s1 und s2 geändert, aber nicht s3 ?

   

String ist unveränderlich *, dies bedeutet jedoch nur, dass Sie sie nicht mithilfe ihrer öffentlichen API ändern können.

Was Sie hier tun, ist die Umgehung der normalen API durch reflection. Auf die gleiche Weise können Sie die Werte von Enums ändern, die Lookup-Tabelle ändern, die in Integer-Autoboxs usw. verwendet wird.

Der Grund, dass s1 und s2 den Wert ändern, besteht darin, dass beide auf dieselbe interne Zeichenfolge verweisen. Der Compiler macht dies (wie von anderen Antworten erwähnt).

Der Grund, warum s3 nicht wirklich überraschend war, war, dass ich dachte, dass es das value Array teilen würde ( in früheren Versionen von Java , vor Java 7u6). Betrachtet man jedoch den Quelltext von String , können wir sehen, dass das Arrays.copyOfRange(..) Array für einen Teilstring tatsächlich kopiert wird (mit Arrays.copyOfRange(..) ). Deshalb bleibt es unverändert.

Sie können einen SecurityManager installieren, um bösartigen Code zu vermeiden. Bedenken Sie jedoch, dass einige Bibliotheken auf diese Art von Reflektionstricks angewiesen sind (normalerweise ORM-Tools, AOP-Bibliotheken usw.).

*) Ich habe ursprünglich geschrieben, dass String s nicht wirklich unveränderlich sind, nur “effektiv unveränderlich”. Dies könnte in der aktuellen Implementierung von String irreführend sein, wo das Wertearray tatsächlich als private final markiert ist. Es ist jedoch immer noch erwähnenswert, dass es keine Möglichkeit gibt, ein Array in Java als unveränderlich zu deklarieren, so dass darauf geachtet werden muss, es nicht außerhalb seiner class verfügbar zu machen, selbst mit den richtigen Zugriffsmodifizierern.


Da dieses Thema überwältigend beliebt ist, hier einige Vorschläge für weitere Lektüre: Heinz Kabutz ‘Reflection Madness-Vortrag von JavaZone 2009, der viele Themen im OP behandelt, zusammen mit anderen Überlegungen … naja … Wahnsinn.

Es deckt ab, warum dies manchmal nützlich ist. Und warum solltest du die meiste Zeit vermeiden. 🙂

Wenn in Java zwei primitive Stringvariablen mit demselben Literal initialisiert werden, weist es beiden Variablen dieselbe Referenz zu:

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

Initialisierung

Das ist der Grund, warum der Vergleich wahr ist. Die dritte Zeichenfolge wird mit substring() , die eine neue Zeichenfolge erstellt, anstatt auf dieselbe zu verweisen.

Unterzeichenfolge

Wenn Sie mit Reflektion auf eine Zeichenfolge zugreifen, erhalten Sie den tatsächlichen pointers:

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

Also ändert sich die Zeichenfolge, die einen pointers darauf hält, aber da s3 aufgrund von substring() mit einer neuen Zeichenfolge erstellt wird, würde es sich nicht ändern.

Veränderung

Sie verwenden Reflektion, um die Unveränderlichkeit von String zu umgehen – es ist eine Form von “Angriff”.

Es gibt viele Beispiele, die Sie so erstellen können (zB können Sie sogar ein Void Objekt instanziieren ), aber das bedeutet nicht, dass String nicht “unveränderbar” ist.

Es gibt Anwendungsfälle, in denen diese Art von Code zu Ihrem Vorteil verwendet werden kann und eine “gute Codierung” ist, z. B. Löschen von Kennwörtern aus dem Speicher zum frühestmöglichen Zeitpunkt (vor dem GC) .

Je nach Sicherheitsmanager können Sie Ihren Code möglicherweise nicht ausführen.

Sie verwenden Reflektion, um auf die “Implementierungsdetails” des String-Objekts zuzugreifen. Unveränderbarkeit ist das Merkmal der öffentlichen Schnittstelle eines Objekts.

Visibility Modifier und Final (dh Unveränderlichkeit) sind keine Messung gegen Schadcode in Java; Sie sind lediglich Werkzeuge, um sich vor Fehlern zu schützen und den Code wartbarer zu machen (eines der großen Verkaufsargumente des Systems). Aus diesem Grund können Sie über Reflektion auf interne Implementierungsdetails wie das Backing-Char-Array für String s zugreifen.

Der zweite Effekt, den Sie sehen, ist, dass sich alle String ändern, während Sie so aussehen, als würden Sie nur s1 ändern. Es ist eine bestimmte Eigenschaft von Java String-Literalen, dass sie automatisch interniert, dh zwischengespeichert werden. Zwei String-Literale mit dem gleichen Wert sind tatsächlich das gleiche Objekt. Wenn Sie einen String mit new erstellen, wird dieser nicht automatisch interniert und Sie sehen diesen Effekt nicht.

#substring bis vor kurzem (Java 7u6) funktionierte in ähnlicher Weise, was das Verhalten in der ursprünglichen Version Ihrer Frage erklärt hätte. Es hat kein neues Backing-Char-Array erstellt, sondern das aus dem ursprünglichen String wiederverwendet. Es wurde gerade ein neues String-Objekt erstellt, das einen Offset und eine Länge verwendet, um nur einen Teil des Arrays darzustellen. Dies funktioniert im Allgemeinen als Strings sind unveränderlich – es sei denn, Sie umgehen dies. Diese Eigenschaft von #substring bedeutete auch, dass der gesamte ursprüngliche String nicht als Garbage #substring erfasst werden konnte, wenn ein kürzerer Teilstring, der daraus erstellt wurde, noch existierte.

Ab dem aktuellen Java und der aktuellen Version der Frage gibt es kein merkwürdiges Verhalten von #substring .

String-Unveränderlichkeit ist aus der Sicht der Schnittstelle. Sie verwenden Reflektion, um die Schnittstelle zu umgehen und die Interna der String-Instanzen direkt zu ändern.

s1 und s2 werden beide geändert, weil sie beide der gleichen “internen” String-Instanz zugewiesen sind. Sie können etwas mehr über diesen Teil von diesem Artikel über Gleichheit der Zeichenkette und Internieren erfahren. Sie werden vielleicht überrascht sein, dass in Ihrem Beispielcode s1 == s2 true zurückgibt!

Welche Version von Java verwenden Sie? Ab Java 1.7.0_06 hat Oracle die interne Darstellung von String geändert, insbesondere die Teilzeichenfolge.

Zitat aus Oracle Tunes Java’s interne String Repräsentation :

Im neuen Paradigma wurden die Felder String offset und count entfernt, so dass Teilstrings nicht mehr den zugrunde liegenden Wert char [] teilen.

Mit dieser Änderung kann es ohne reflection (???) passieren.

Hier sind wirklich zwei Fragen:

  1. Sind Strings wirklich unveränderlich?
  2. Warum wird s3 nicht geändert?

Zu Punkt 1: Außer ROM gibt es keinen unveränderlichen Speicher in Ihrem Computer. Heutzutage ist manchmal sogar ROM beschreibbar. Es gibt immer irgendwo Code (ob es der coreel oder der native Code ist, der Ihre verwaltete Umgebung umgeht), der in Ihre Speicheradresse schreiben kann. In der “Realität” sind sie also nicht absolut unveränderlich.

Zu Punkt 2: Das liegt daran, dass die Teilzeichenfolge wahrscheinlich eine neue Zeichenfolgeninstanz zuweist, die wahrscheinlich das Array kopiert. Es ist möglich, einen Teilstring so zu implementieren, dass er keine Kopie macht, aber das bedeutet nicht, dass er es tut. Es gibt Kompromisse.

Zum Beispiel, sollte ein Verweis auf reallyLargeString.substring(reallyLargeString.length - 2) führen, dass eine große Menge an Arbeitsspeicher am Leben gehalten wird, oder nur ein paar Bytes?

Das hängt davon ab, wie die Teilzeichenfolge implementiert wird. Eine tiefe Kopie wird weniger Speicher am Leben erhalten, aber es wird etwas langsamer laufen. Eine flache Kopie wird mehr Speicher am Leben erhalten, aber es wird schneller sein. Die Verwendung einer tiefen Kopie kann auch die Heap-Fragmentierung reduzieren, da das String-Objekt und sein Puffer in einem Block zugewiesen werden können, im Gegensatz zu 2 separaten Heap-Zuordnungen.

In jedem Fall sieht es so aus, als ob Ihre JVM tiefe Kopien für Teilstring-Aufrufe verwendet hat.

Um die Antwort von @ haraldK hinzuzufügen – dies ist ein Sicherheits-Hack, der zu einer ernsthaften Beeinträchtigung der App führen könnte.

Das erste ist eine Änderung an einer konstanten Zeichenfolge, die in einem String-Pool gespeichert ist. Wenn String als String s = "Hello World"; deklariert ist String s = "Hello World"; Es wird in einen speziellen Objektpool für weitere mögliche Wiederverwendung platziert. Das Problem besteht darin, dass der Compiler während der Kompilierung einen Verweis auf die geänderte Version speichert. Sobald der Benutzer die in diesem Pool gespeicherte Zeichenfolge zur Laufzeit ändert, zeigen alle Referenzen im Code auf die geänderte Version. Dies würde zu einem folgenden Fehler führen:

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

Wird drucken:

 Hello Java! 

Es gab ein anderes Problem, das ich erlebte, als ich eine schwere Berechnung über solche riskanten Strings implementierte. Es gab einen Fehler, der während der Berechnung in 1 von 1000000 Fällen auftrat, was das Ergebnis unbestimmbar machte. Ich konnte das Problem finden, indem ich den JIT abstellte – ich bekam immer das gleiche Ergebnis, wenn JIT ausgeschaltet war. Meine Vermutung ist, dass der Grund für diesen String-Sicherheits-Hack einige der JIT-Optimierungsverträge gebrochen hat.

Gemäß dem Pooling-Konzept zeigen alle String-Variablen, die den gleichen Wert enthalten, auf die gleiche Speicheradresse. Daher zeigen s1 und s2, die beide den gleichen Wert von “Hello World” enthalten, auf denselben Speicherplatz (z. B. M1).

Auf der anderen Seite, s3 enthält “World”, daher wird es auf eine andere Speicherzuweisung zeigen (sagen wir M2).

Also, was passiert, ist, dass der Wert von S1 geändert wird (mit dem char [] Wert). Somit wurde der Wert an der Speicherstelle M1, auf den sowohl s1 als auch s2 zeigen, geändert.

Folglich wurde der Speicherplatz M1 modifiziert, was eine Änderung des Wertes von s1 und s2 verursacht.

Der Wert der Position M2 bleibt jedoch unverändert, daher enthält s3 denselben ursprünglichen Wert.

Der Grund, warum sich s3 nicht wirklich ändert, liegt darin, dass in Java beim Erstellen einer Teilzeichenfolge das Wertezeichen-Array für eine Teilzeichenfolge intern kopiert wird (mithilfe von Arrays.copyOfRange ()).

s1 und s2 sind gleich, weil sie sich in Java auf den gleichen internierten String beziehen. Es ist von Entwurf in Java.

String ist unveränderbar, aber durch Reflektion darf die String-class geändert werden. Sie haben die String-class in Echtzeit als veränderbar definiert. Sie können Methoden als öffentlich oder privat oder statisch definieren, wenn Sie möchten.

[Disclaimer dies ist eine absichtlich eigensinnige Art der Antwort, wie ich fühle, ein mehr “tun dies nicht zu Hause Kinder” Antwort ist gerechtfertigt]

Die Sünde ist die Zeile field.setAccessible(true); was besagt, die öffentliche API zu verletzen, indem man Zugang zu einem privaten Feld gewährt. Das ist ein riesiges Sicherheitsloch, das durch Konfigurieren eines Sicherheits-Managers gesperrt werden kann.

Das Phänomen in der Frage sind Implementierungsdetails, die Sie nie sehen würden, wenn Sie diese gefährliche Codezeile nicht verwenden, um die Zugriffsmodifizierer durch Reflektion zu verletzen. Offensichtlich können sich zwei (normalerweise) unveränderliche Strings dasselbe Char-Array teilen. Ob ein Teilstring dasselbe Array teilt, hängt davon ab, ob er es kann und ob der Entwickler dachte, es zu teilen. Normalerweise sind dies unsichtbare Implementierungsdetails, die Sie nicht kennen sollten, es sei denn, Sie nehmen den Zugriffsmodifizierer über den Kopf mit dieser Codezeile auf.

Es ist einfach keine gute Idee, sich auf solche Details zu verlassen, die nicht erlebt werden können, ohne die Zugriffsmodifizierer durch reflection zu verletzen. Der Besitzer dieser class unterstützt nur die normale öffentliche API und kann in der Zukunft Implementierungsänderungen vornehmen.

Nachdem Sie gesagt haben, dass die Codezeile wirklich sehr nützlich ist, wenn Sie eine Waffe in der Hand halten, zwingt Sie Ihr Kopf dazu, solche gefährlichen Dinge zu tun. Die Verwendung dieser Hintertür ist normalerweise ein Code-Geruch, den Sie auf einen besseren Bibliothekscode aufrüsten müssen, wo Sie nicht sündigen müssen. Eine andere häufige Verwendung dieser gefährlichen Codezeile ist das Schreiben eines “Voodoo-Frameworks” (Orm, Injektionscontainer, …). Viele Leute werden religiös über solche Rahmenbedingungen (sowohl für als auch gegen sie), so dass ich es vermeiden werde, einen Flammenkrieg einzuladen, indem ich nichts anderes sage, als dass die große Mehrheit der Programmierer nicht dorthin gehen muss.

Strings werden im permanenten Bereich des JVM-Heap-Speichers erstellt. Also ja, es ist wirklich unveränderlich und kann nicht geändert werden, nachdem es erstellt wurde. Denn in der JVM gibt es drei Arten von Heap-Speicher: 1. Junge Generation 2. Alte Generation 3. Permanente Generation.

Wenn ein Objekt erstellt wird, wird es in den Heap-Bereich der jungen Generation und in den PermGen-Bereich übernommen, der für das String-Pooling reserviert ist.

Hier finden Sie weitere Details, über die Sie weitere Informationen abrufen können: Wie Garbage Collection in Java funktioniert .