wake-up-neo.com

Ist eine Java-Zeichenfolge wirklich unveränderlich?

Wir alle wissen, dass String in Java nicht veränderbar ist, überprüfen Sie jedoch 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?

381
Darshan Patel

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

Was Sie hier tun, umgeht die normale API mit Reflektion. Auf dieselbe Weise können Sie die Werte von Enums ändern, die in Integer-Autoboxing verwendete Nachschlagetabelle usw. ändern.

Der Grund für den s1- und s2-Änderungswert ist, dass beide sich auf dieselbe interne Zeichenfolge beziehen. Der Compiler macht dies (wie in anderen Antworten erwähnt). 

Der Grund s3 tut not war für mich eigentlich ein wenig überraschend, da ich dachte, er würde das value-Array ( in früheren Versionen von Java vor Java 7u6) verwenden. Wenn Sie jedoch den Quellcode von String betrachten, können wir feststellen, dass das value-Zeichenarray für einen Teilstring tatsächlich kopiert wird (mithilfe von Arrays.copyOfRange(..)). Deshalb geht es unverändert weiter.

Sie können eine SecurityManager installieren, um zu verhindern, dass böswilliger Code solche Aktionen ausführt. Beachten Sie jedoch, dass einige Bibliotheken auf diese Art von Reflektionstricks angewiesen sind (normalerweise ORM-Tools, AOP-Bibliotheken usw.).

*) Ich habe anfangs geschrieben, dass Strings nicht wirklich unveränderlich sind, sondern nur "effektiv unveränderlich". Dies kann in der aktuellen Implementierung von String irreführend sein, wobei das value-Array tatsächlich mit private final markiert ist. Es ist jedoch immer noch erwähnenswert, dass es nicht möglich ist, ein Array in Java als unveränderlich zu deklarieren. Daher muss darauf geachtet werden, dass es nicht außerhalb seiner Klasse angezeigt wird, selbst wenn die entsprechenden Zugriffsmodifizierer verwendet werden.


Da dieses Thema überaus populär erscheint, schlagen wir einige weiterführende Lektüre vor: Heinz Kabutz's Reflection Madness-Vortrag aus JavaZone 2009, die viele Themen des OP und andere Überlegungen abdeckt ... naja ... Wahnsinn. 

Es wird erläutert, warum dies manchmal nützlich ist. Und warum sollten Sie es meistens vermeiden? :-)

394
haraldK

Wenn in Java zwei primitive String-Variablen mit demselben Literal initialisiert werden, weist sie beiden Variablen dieselbe Referenz zu:

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

initialization

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

sub string

Wenn Sie mit Reflektion auf einen String zugreifen, erhalten Sie den tatsächlichen Zeiger:

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

Wenn Sie dies ändern, ändert sich die Zeichenfolge, die einen Zeiger darauf enthält. Wenn s3 jedoch aufgrund von substring() mit einer neuen Zeichenfolge erstellt wird, ändert sich dies nicht.

change

94
Zaheer Ahmed

Sie verwenden Reflektion, um die Unveränderlichkeit von String zu umgehen - es ist eine Form von "Angriff".

Es gibt viele Beispiele, die Sie auf diese Weise erstellen können (zB Sie können auch ein Void-Objekt instanziieren), aber das bedeutet nicht, dass String nicht "unveränderlich" ist.

Es gibt Anwendungsfälle, in denen diese Art von Code zu Ihrem Vorteil verwendet werden kann und eine "gute Codierung" ist, wie zum Beispiel das Löschen von Passwörtern zum frühestmöglichen Zeitpunkt (vor der GC) .

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

50
Bohemian

Sie verwenden Reflection, um auf die "Implementierungsdetails" des String-Objekts zuzugreifen. Unveränderlichkeit ist das Merkmal der öffentlichen Schnittstelle eines Objekts.

30
Ankur

Sichtbarkeitsmodifizierer und Endgültigkeit (d. H. Unveränderlichkeit) sind kein Maß gegen bösartigen Code 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 Reflection auf interne Implementierungsdetails wie das Backing-Char-Array für Strings zugreifen.

Der zweite Effekt, den Sie sehen, ist, dass sich alle Strings ändern, während es so aussieht, als würden Sie nur s1 ändern. Es ist eine bestimmte Eigenschaft von Java-String-Literalen, dass sie automatisch interniert werden, d. H. Zwischengespeichert werden. Zwei String-Literale mit demselben Wert sind tatsächlich dasselbe Objekt. Wenn Sie einen String mit new erstellen, wird er nicht automatisch interniert und Sie sehen diesen Effekt nicht.

#substring (Java 7u6) funktionierte bis vor kurzem auf ähnliche 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 dieses Arrays darzustellen. Dies funktioniert im Allgemeinen, da Strings unveränderlich sind - sofern Sie dies nicht umgehen. Diese Eigenschaft von #substring bedeutete auch, dass die gesamte ursprüngliche Zeichenfolge nicht als Garbage Collection erfasst werden konnte, wenn eine kürzere Teilzeichenfolge noch vorhanden war.

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

24

Zeichenfolge-Unveränderlichkeit ist aus Sicht der Benutzeroberfläche. Sie verwenden Reflection, um die Schnittstelle zu umgehen und die Internen der String-Instanzen direkt zu ändern.

s1 und s2 werden geändert, da beide derselben "internen" String-Instanz zugeordnet sind. Mehr über diesen Teil erfahren Sie in diesem Artikel über String-Gleichheit und Interning. Sie könnten überrascht sein, dass s1 == s2 in Ihrem Beispielcode true zurückgibt!

11
Krease

Welche Java-Version verwenden Sie? Von Java 1.7.0_06 aus hat Oracle die interne Darstellung von String geändert, insbesondere den Teilstring. 

Zitieren von Interne Zeichenkettenrepräsentation von Oracle Tunes Java :

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

Mit dieser Änderung kann es ohne Reflexion (???) geschehen.

10
manikanta

Hier gibt es wirklich zwei Fragen:

  1. Sind Strings wirklich unveränderlich?
  2. Warum wird S3 nicht verändert?

Zu Punkt 1: Außer ROM befindet sich auf Ihrem Computer kein unveränderlicher Speicher. Heutzutage ist sogar ROM manchmal beschreibbar. Es gibt immer irgendwo Code (egal ob der Kernel oder der native Code, der Ihre verwaltete Umgebung umgeht), der in Ihre Speicheradresse schreiben kann. Also in der "Realität" sind sie nicht absolut unveränderlich.

Zu Punkt 2: Dies liegt daran, dass der Teilstring wahrscheinlich eine neue Zeichenfolgeninstanz zuweist, wodurch das Array wahrscheinlich kopiert wird. Es ist möglich, die Teilzeichenfolge so zu implementieren, dass keine Kopie erstellt wird. Dies bedeutet jedoch nicht, dass dies der Fall ist. Es gibt Kompromisse.

Soll beispielsweise ein Verweis auf reallyLargeString.substring(reallyLargeString.length - 2) dazu führen, dass eine große Menge Speicher oder nur wenige Bytes lebendig gehalten wird?

Das hängt davon ab, wie der Teilstring implementiert wird. Bei einer tiefen Kopie bleibt zwar weniger Speicherplatz erhalten, sie läuft jedoch etwas langsamer. Eine flache Kopie hält mehr Speicher am Leben, ist aber schneller. Die Verwendung einer tiefen Kopie kann auch die Heap-Fragmentierung reduzieren, da das String-Objekt und sein Puffer im Gegensatz zu zwei separaten Heap-Zuordnungen in einem Block zugewiesen werden können.

In jedem Fall sieht es so aus, als würde Ihre JVM tiefe Kopien für Teilzeichenfolgenaufrufe verwenden.

7

Um die Antwort von @ haraldK zu ergänzen, handelt es sich um einen Sicherheits-Hack, der zu gravierenden Auswirkungen in der App führen kann. 

Das erste ist eine Änderung an einem konstanten String, der in einem String-Pool gespeichert ist. Wenn string als String s = "Hello World"; deklariert ist, wird es zur weiteren Wiederverwendung in einen speziellen Objektpool gestellt. Das Problem ist, dass der Compiler zur Kompilierzeit einen Verweis auf die geänderte Version platziert. Sobald der Benutzer die in diesem Pool gespeicherte Zeichenfolge zur Laufzeit geändert hat, zeigen alle Verweise 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 mir auftrat, als ich bei solchen riskanten Strings eine umfangreiche Berechnung durchführte. Es gab einen Fehler, der in etwa 1 von 1000000-mal während der Berechnung auftrat, was das Ergebnis unbestimmt machte. Ich konnte das Problem durch Ausschalten der JIT feststellen - ich hatte immer das gleiche Ergebnis, wenn JIT ausgeschaltet war. Meine Vermutung ist, dass der Grund für diesen String-Sicherheits-Hack war, der einige der JIT-Optimierungsverträge gebrochen hat.

5
Andrey Chaschev

Gemäß dem Konzept des Poolings zeigen alle String-Variablen, die denselben Wert enthalten, auf dieselbe 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 enthält s3 "World", daher weist es auf eine andere Speicherzuordnung hin (beispielsweise M2).

Was jetzt passiert, ist, dass der Wert von S1 geändert wird (unter Verwendung des char [] -Werts). Der Wert an der Speicherstelle M1, auf den sowohl s1 als auch s2 zeigen, wurde 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 den gleichen ursprünglichen Wert.

5
AbhijeetMishra

Der Grund für die Änderung von s3 ist, dass in Java bei einer Teilzeichenfolge das Wertzeichen-Array für eine Teilzeichenfolge intern kopiert wird (mit Arrays.copyOfRange ()).

s1 und s2 sind gleich, da sie sich in Java auf dieselbe interne Zeichenfolge beziehen. Es ist beabsichtigt in Java.

4

String ist unveränderlich, aber durch Reflektion können Sie die String-Klasse ändern. Sie haben gerade die String-Klasse in Echtzeit als veränderlich definiert. Wenn Sie möchten, können Sie die Methoden als öffentliche, private oder statische Methoden definieren.

2
SpacePrez

[Haftungsausschluss Dies ist eine absichtlich meinungsbewusste Art der Antwort, da ich der Meinung bin, dass es "mehr nicht zu Hause tun soll". 

Die Sünde ist die Linie field.setAccessible(true);, die besagt, dass sie gegen die öffentliche API verstößt, indem sie den Zugriff auf ein privates Feld erlaubt. Das ist eine riesige Sicherheitslücke, die durch Konfigurieren eines Sicherheitsmanagers gesperrt werden kann. 

Das Phänomen in der Frage sind Implementierungsdetails, die Sie niemals sehen würden, wenn Sie diese gefährliche Codezeile nicht verwenden, um die Zugriffsmodifizierer durch Reflektion zu verletzen. Zweifellos können zwei (normalerweise) unveränderliche Zeichenfolgen dasselbe Zeichenarray verwenden. Ob ein Teilstring dasselbe Array gemeinsam verwendet, hängt davon ab, ob dies möglich ist und ob der Entwickler das Teil gemeinsam nutzen wollte. Normalerweise handelt es sich um unsichtbare Implementierungsdetails, die Sie nicht kennen sollten, wenn Sie den Zugriffsmodifizierer mit dieser Codezeile durch den Kopf schießen. 

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

Nachdem Sie alle gesagt haben, dass die Codezeile wirklich sehr nützlich ist, wenn Sie von einer Waffe gehalten werden, die Sie den Kopf zwingt, so gefährliche Dinge zu tun. Die Verwendung dieser Hintertür ist normalerweise ein Codegeruch, den Sie aktualisieren müssen, um den Bibliothekscode zu verbessern, an dem Sie nicht sündigen müssen. Eine weitere häufige Verwendung dieser gefährlichen Codezeile ist das Schreiben eines "Voodoo-Frameworks" (orm, Injektionscontainer, ...). Viele Leute sind über solche Rahmenbedingungen (sowohl für als auch gegen sie) religiös, daher werde ich es vermeiden, einen Flammenkrieg einzuladen, indem ich nichts anderes sage, als die große Mehrheit der Programmierer muss nicht dorthin gehen. 

1
simbo1905

Zeichenfolgen werden im permanenten Bereich des JVM-Heap-Speichers erstellt. Ja, es ist wirklich unveränderlich und kann nach dem Erstellen nicht mehr geändert werden. Da es in der JVM drei Arten von Heapspeicher gibt: 1. Junge Generation 2. Alte Generation 3. Dauerhafte Erzeugung.

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

Hier können Sie weitere Informationen abrufen: So funktioniert die Garbage Collection in Java.

String ist in der Natur unveränderlich, da es keine Methode gibt, das String-Objekt zu ändern. Dies ist der Grund, warum Sie StringBuilder und StringBuffer classes eingeführt haben 

0