Wir alle wissen, dass dies String
in Java unveränderlich 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 ist der Wert von s1
und s2
verändert, aber nicht s3
?
java
string
reflection
immutability
Darshan Patel
quelle
quelle
(Integer)1+(Integer)2=42
mit dem zwischengespeicherten Autoboxing herumspielen. (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )Antworten:
String
ist unveränderlich *, aber dies bedeutet nur, dass Sie es nicht mit seiner öffentlichen API ändern können.Was Sie hier tun, ist die Umgehung der normalen API mithilfe von Reflektion. Auf die gleiche Weise können Sie die Werte von Aufzählungen ändern, die beim Integer-Autoboxing verwendete Nachschlagetabelle ändern usw.
Der Grund
s1
und ders2
Änderungswert besteht nun darin, dass beide auf dieselbe internierte Zeichenfolge verweisen. Der Compiler tut dies (wie in anderen Antworten erwähnt).Der Grund dafür
s3
ist nicht eigentlich ein bisschen zu mir überraschend, da ich dachte , es würde den Anteilvalue
Array ( es hat in früheren Version von Java , bevor Java 7u6). WennString
wir uns jedoch den Quellcode von ansehen, können wir sehen, dass dasvalue
Zeichenarray für einen Teilstring tatsächlich kopiert wird (mitArrays.copyOfRange(..)
). Deshalb bleibt es unverändert.Sie können einen installieren
SecurityManager
, um bösartigen Code zu vermeiden, um solche Dinge zu tun. Beachten Sie jedoch, dass einige Bibliotheken von der Verwendung dieser Art von Reflexionstricks abhängen (normalerweise ORM-Tools, AOP-Bibliotheken usw.).*) Ich schrieb anfangs, dass
String
s nicht wirklich unveränderlich sind, sondern nur "effektiv unveränderlich". Dies kann in der aktuellen Implementierung von irreführend seinString
, wenn dasvalue
Array tatsächlich markiert istprivate final
. Es ist jedoch immer noch erwähnenswert, dass es keine Möglichkeit gibt, ein Array in Java als unveränderlich zu deklarieren. Daher muss darauf geachtet werden, dass es auch mit den richtigen Zugriffsmodifikatoren nicht außerhalb seiner Klasse verfügbar gemacht wird.Da dieses Thema überwältigend beliebt zu sein scheint, wird hier eine weitere Lektüre vorgeschlagen: Heinz Kabutz 'Reflection Madness-Vortrag aus der JavaZone 2009, der viele Themen im OP zusammen mit anderen Überlegungen behandelt ... na ja ... Wahnsinn.
Es wird erläutert, warum dies manchmal nützlich ist. Und warum sollten Sie es die meiste Zeit vermeiden? :-)
quelle
String
Internieren Teil des JLS ( "ein String-Literal bezieht sich immer auf dieselbe Instanz der Klasse String" ). Aber ich stimme zu, es ist keine gute Praxis, sich auf die Implementierungsdetails derString
Klasse zu verlassen.substring
Kopien statt eines "Abschnitts" des vorhandenen Arrays verwendet werden, darin, dass das riesige Array am Leben bleiben würde, wenn ich einen riesigen String hättes
und einen winzigen Teilstring herausnehmen würde, dert
von ihm aufgerufen wurde , und ihn später aufgab,s
aber behieltt
(kein Müll gesammelt). Vielleicht ist es natürlicher, wenn jedem Zeichenfolgenwert ein eigenes Array zugeordnet ist?String
Instanz Variablen tragen musste, um den Offset in das angegebene Array und die angegebene Länge zu speichern. Dies ist ein Overhead, den man angesichts der Gesamtzahl der Zeichenfolgen und des typischen Verhältnisses zwischen normalen Zeichenfolgen und Teilzeichenfolgen in einer Anwendung nicht ignorieren sollte. Da sie für jede Zeichenfolgenoperation ausgewertet werden mussten, bedeutete dies, dass jede Zeichenfolgenoperation nur zum Vorteil einer einzigen Operation, einer billigen Teilzeichenfolge , verlangsamt wurde .byte[]
Zeichenfolgen für ASCII-Zeichenfolgen undchar[]
für andere haben. Dies bedeutet, dass bei jeder Operation überprüft werden muss, um welche Art von Zeichenfolge es sich handelt Betriebs. Dies behindert das Einfügen des Codes in die Methoden mithilfe von Zeichenfolgen. Dies ist der erste Schritt weiterer Optimierungen unter Verwendung der Kontextinformationen des Aufrufers. Dies ist eine große Auswirkung.Wenn in Java zwei primitive Zeichenfolgenvariablen mit demselben Literal initialisiert werden, wird beiden Variablen dieselbe Referenz zugewiesen:
Aus diesem Grund gibt der Vergleich true zurück. Die dritte Zeichenfolge wird erstellt, mit
substring()
der eine neue Zeichenfolge erstellt wird, anstatt auf dieselbe zu zeigen.Wenn Sie mit Reflektion auf eine Zeichenfolge zugreifen, erhalten Sie den tatsächlichen Zeiger:
Wenn Sie dies ändern, wird die Zeichenfolge geändert, die einen Zeiger darauf enthält. Da dies jedoch
s3
mit einer neuen Zeichenfolge erstellt wird,substring()
ändert sich dies nicht.quelle
intern
eine nicht-wörtliche Zeichenfolge auch manuell aufrufen und die Vorteile nutzen.intern
Bedacht verwenden. Wenn Sie alles internieren, gewinnen Sie nicht viel und können einige kratzende Momente verursachen, wenn Sie der Mischung Reflexion hinzufügen.Test1
undTest1
sind nicht mittest1==test2
den Java-Namenskonventionen vereinbar und folgen diesen nicht.Sie verwenden Reflexion, um die Unveränderlichkeit von String zu umgehen - es ist eine Form von "Angriff".
Es gibt viele Beispiele, die Sie so erstellen können (z. B. können Sie sogar 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" darstellt, z. B. das Löschen von Kennwörtern aus dem Speicher zum frühestmöglichen Zeitpunkt (vor der GC) .
Abhängig vom Sicherheitsmanager können Sie Ihren Code möglicherweise nicht ausführen.
quelle
Sie verwenden Reflection, um auf die "Implementierungsdetails" des Zeichenfolgenobjekts zuzugreifen. Unveränderlichkeit ist das Merkmal der öffentlichen Schnittstelle eines Objekts.
quelle
Sichtbarkeitsmodifikatoren und final (dh Unveränderlichkeit) sind keine Messung gegen schädlichen 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
String
über Reflection auf interne Implementierungsdetails wie das Backing Char Array für s zugreifen.Der zweite Effekt, den Sie sehen, ist, dass sich alle
String
ändern, während es so aussieht, als würden Sie sich nur änderns1
. Es ist eine bestimmte Eigenschaft von Java-String-Literalen, dass sie automatisch interniert, dh zwischengespeichert werden. Zwei String-Literale mit demselben Wert sind tatsächlich dasselbe Objekt. Wenn Sie einen Stringnew
damit erstellen , wird dieser nicht automatisch interniert und Sie sehen diesen Effekt nicht.#substring
Bis vor kurzem funktionierte (Java 7u6) auf ähnliche Weise, was das Verhalten in der Originalversion Ihrer Frage erklärt hätte. Es wurde kein neues Backing-Char-Array erstellt, sondern das aus dem ursprünglichen String wiederverwendet. Es wurde gerade ein neues String-Objekt erstellt, das einen Versatz und eine Länge verwendete, um nur einen Teil dieses Arrays darzustellen. Dies funktionierte im Allgemeinen, da Strings unveränderlich sind - es sei denn, Sie umgehen dies. Diese Eigenschaft von#substring
bedeutete auch, dass der gesamte ursprüngliche String nicht als Müll gesammelt werden konnte, wenn noch ein kürzerer Teilstring vorhanden war, der daraus erstellt wurde.Ab dem aktuellen Java und Ihrer aktuellen Version der Frage gibt es kein merkwürdiges Verhalten von
#substring
.quelle
final
auch durch Reflexion zu verletzen . Wie in einer anderen Antwort erwähnt,#substring
teilen Java 7u6 keine Arrays.final
im Laufe der Zeit geändert ...: -O Laut dem "Reflection Madness" -Vortrag von Heinz, den ich im anderen Thread gepostet habe,final
bedeutete dies endgültig in JDK 1.1, 1.3 und 1.4, konnte aber immer mit Reflection unter Verwendung von 1.2 geändert werden und in 1.5 und 6 in den meisten Fällen ...final
Felder können durchnative
Code geändert werden , wie dies vom Serialisierungsframework beim Lesen der Felder einer serialisierten Instanz durchgeführt wird undSystem.setOut(…)
das die endgültigeSystem.out
Variable ändert . Letzteres ist das interessanteste Merkmal, da die Reflexion mit Zugriffsüberschreibung diestatic final
Felder nicht ändern kann .Die Unveränderlichkeit von Zeichenfolgen erfolgt aus Sicht der Benutzeroberfläche. Sie verwenden Reflection, um die Schnittstelle zu umgehen und die Interna der String-Instanzen direkt zu ändern.
s1
unds2
werden beide geändert, weil sie beide derselben "internen" String-Instanz zugewiesen sind. Weitere Informationen zu diesem Teil finden Sie in diesem Artikel über String-Gleichheit und Internierung. Sie könnten überrascht sein , dass in Ihrem Beispielcode , um herauszufinden,s1 == s2
kehrttrue
!quelle
Welche Java-Version verwenden Sie? Ab Java 1.7.0_06 hat Oracle die interne Darstellung von String geändert, insbesondere die Teilzeichenfolge.
Zitieren aus der internen Zeichenfolgendarstellung von Oracle Tunes Java :
Mit dieser Änderung kann es ohne Reflexion geschehen (???).
quelle
Hier gibt es wirklich zwei Fragen:
Zu Punkt 1: Mit Ausnahme des ROM befindet sich auf Ihrem Computer kein unveränderlicher Speicher. Heutzutage ist sogar ROM manchmal beschreibbar. Es gibt immer irgendwo Code (egal ob es sich um den Kernel oder den nativen Code handelt, der Ihre verwaltete Umgebung umgeht), der in Ihre Speicheradresse schreiben kann. Also, in "Realität", nein, sie sind nicht absolut unveränderlich.
Zu Punkt 2: Dies liegt daran, dass der Teilstring wahrscheinlich eine neue Zeichenfolgeninstanz zuweist, die wahrscheinlich das Array kopiert. Es ist möglich, Teilzeichenfolgen so zu implementieren, dass keine Kopie erstellt wird, dies bedeutet jedoch nicht, dass dies der Fall ist. Es gibt Kompromisse.
Sollte beispielsweise eine Referenz gehalten werden,
reallyLargeString.substring(reallyLargeString.length - 2)
um eine große Menge an Speicher am Leben zu erhalten, oder nur wenige Bytes?Das hängt davon ab, wie die Teilzeichenfolge implementiert wird. Eine tiefe Kopie hält weniger Speicher am Leben, 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 Zeichenfolgenobjekt und sein Puffer in einem Block zugewiesen werden können, im Gegensatz zu zwei separaten Heap-Zuweisungen.
In jedem Fall scheint Ihre JVM Deep Copies für Teilstring-Aufrufe verwendet zu haben.
quelle
Um die Antwort von @ haraldK zu ergänzen: Dies ist ein Sicherheitshack, der zu schwerwiegenden Auswirkungen auf die App führen kann.
Als erstes wird eine konstante Zeichenfolge geändert, die in einem Zeichenfolgenpool gespeichert ist. Wenn eine Zeichenfolge als deklariert wird
String s = "Hello World";
, wird sie zur weiteren potenziellen Wiederverwendung in einen speziellen Objektpool eingefügt. Das Problem ist, dass der Compiler zur Kompilierungszeit einen Verweis auf die geänderte Version platziert. Sobald der Benutzer die in diesem Pool zur Laufzeit gespeicherte Zeichenfolge ändert, verweisen alle Verweise im Code auf die geänderte Version. Dies würde zu einem folgenden Fehler führen:Wird drucken:
Es gab ein anderes Problem, das ich hatte, als ich eine schwere Berechnung über solch riskante Zeichenfolgen implementierte. Es gab einen Fehler, der während der Berechnung etwa 1 von 1000000 Mal auftrat und das Ergebnis unbestimmt machte. Ich konnte das Problem durch Ausschalten der JIT finden - bei ausgeschalteter JIT wurde immer das gleiche Ergebnis erzielt. Ich vermute, dass der Grund dieser String-Sicherheits-Hack war, der einige der JIT-Optimierungsverträge gebrochen hat.
quelle
String.format("")
in eine der inneren Schleifen einzufügen. Es besteht die Möglichkeit, dass es sich um ein anderes als ein JIT-Fehler handelt, aber ich glaube, es war ein JIT, da dieses Problem nach dem Hinzufügen dieses No-Op nie wieder reproduziert wurde.String
Interna , ist Ihnen nicht in den Sinn gekommen?final
Sicherheitsgarantien des Feldthreads stützt , die beim Ändern der Daten nach der Objektkonstruktion brechen. Sie können es also nach Belieben als JIT- oder MT-Problem anzeigen. Das eigentliche Problem besteht darin,String
Daten zu hacken und zu ändern, von denen erwartet wird, dass sie unveränderlich sind.Gemäß dem Konzept des Poolings verweisen 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 Speicherort (z. B. M1).
Auf der anderen Seite enthält s3 "Welt", daher weist es auf eine andere Speicherzuordnung hin (z. B. M2).
Was jetzt passiert ist, dass der Wert von S1 geändert wird (unter Verwendung des char [] -Werts). Daher wurde der Wert an dem Speicherplatz M1, auf den sowohl s1 als auch s2 zeigen, geändert.
Infolgedessen wurde der Speicherort M1 modifiziert, was eine Änderung des Wertes von s1 und s2 bewirkt.
Der Wert des Ortes M2 bleibt jedoch unverändert, daher enthält s3 den gleichen ursprünglichen Wert.
quelle
Der Grund, warum sich s3 nicht ändert, ist, dass in Java beim Erstellen eines Teilstrings das Wertzeichenarray für einen Teilstring intern kopiert wird (mithilfe von Arrays.copyOfRange ()).
s1 und s2 sind identisch, da beide in Java auf dieselbe internierte Zeichenfolge verweisen. Es ist beabsichtigt in Java.
quelle
String.substring(int, int)
mit Java 7u6 geändert. Vor 7u6 würde die JVM halten nur einen Zeiger auf den ursprünglichenString
‚schar[]
zusammen mit einem Index und Länge. Nach 7u6 wird der Teilstring in einen neuen kopiert.String
Es gibt Vor- und Nachteile.String ist unveränderlich, aber durch Reflexion können Sie die String-Klasse ändern. Sie haben gerade die String-Klasse in Echtzeit als veränderbar neu definiert. Sie können Methoden neu definieren, um öffentlich oder privat oder statisch zu sein, wenn Sie möchten.
quelle
[Haftungsausschluss Dies ist eine absichtlich meinungsgebundene Antwort, da ich der Meinung bin, dass eine Antwort "Mach das nicht zu Hause, Kinder" gerechtfertigt ist.]
Die Sünde ist die Linie
field.setAccessible(true);
die besagt, dass die öffentliche API verletzt werden soll, indem der Zugang zu einem privaten Feld ermöglicht wird. Das ist eine riesige Sicherheitslücke, die durch die Konfiguration eines Sicherheitsmanagers geschlossen werden kann.Das Phänomen in der Frage sind Implementierungsdetails, die Sie niemals sehen würden, wenn Sie diese gefährliche Codezeile nicht verwenden würden, um die Zugriffsmodifikatoren durch Reflexion zu verletzen. Es ist klar, dass zwei (normalerweise) unveränderliche Zeichenfolgen dasselbe Zeichenarray verwenden können. Ob ein Teilstring dasselbe Array verwendet, hängt davon ab, ob dies möglich ist und ob der Entwickler daran gedacht hat, es gemeinsam zu nutzen. Normalerweise sind dies unsichtbare Implementierungsdetails, die Sie nicht kennen sollten, es sei denn, Sie schießen den Zugriffsmodifikator mit dieser Codezeile durch den Kopf.
Es ist einfach keine gute Idee, sich auf solche Details zu verlassen, die nicht erlebt werden können, ohne die Zugriffsmodifikatoren durch Reflexion zu verletzen. Der Eigentümer dieser Klasse unterstützt nur die normale öffentliche API und kann künftig Implementierungsänderungen vornehmen.
Abgesehen davon ist die Codezeile wirklich sehr nützlich, wenn Sie eine Waffe haben, die Sie am Kopf hält und Sie dazu zwingt, so gefährliche Dinge zu tun. Die Verwendung dieser Hintertür ist normalerweise ein Codegeruch, den Sie aktualisieren müssen, um einen besseren Bibliothekscode zu erhalten, bei dem 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 in Bezug auf solche Rahmenbedingungen (sowohl für als auch gegen sie), daher werde ich es vermeiden, einen Flammenkrieg einzuladen, indem ich nichts anderes sage, als dass die überwiegende Mehrheit der Programmierer nicht dorthin gehen muss.
quelle
Zeichenfolgen werden im permanenten Bereich des JVM-Heapspeichers erstellt. Also ja, es ist wirklich unveränderlich und kann nach der Erstellung nicht mehr geändert werden. Denn in der JVM gibt es drei Arten von Heapspeicher: 1. Junge Generation 2. Alte Generation 3. Permanente Generation.
Wenn ein Objekt erstellt wird, wird es in den Heap-Bereich der jungen Generation und den PermGen-Bereich verschoben, die für das String-Pooling reserviert sind.
Hier finden Sie weitere Informationen, aus denen Sie weitere Informationen abrufen können: Funktionsweise der Garbage Collection in Java .
quelle
String ist von Natur aus unveränderlich Da es keine Methode zum Ändern des String-Objekts gibt. Aus diesem Grund haben sie die Klassen StringBuilder und StringBuffer eingeführt
quelle