Was würde wirklich passieren, wenn java.lang.String nicht endgültig wäre?

16

Ich bin ein langjähriger Java-Entwickler und habe nach dem Hauptfach Zeit, es anständig zu studieren, um die Zertifizierungsprüfung abzulegen. Eine Sache, die mich immer gestört hat, ist, dass String "endgültig" ist. Ich verstehe es, wenn ich etwas über Sicherheitsprobleme und ähnliches lese ... Aber im Ernst, hat jemand ein echtes Beispiel dafür?

Was würde zum Beispiel passieren, wenn String nicht final wäre? Als wäre es nicht in Ruby. Ich habe keine Beschwerden von der Ruby-Community gehört ... Und mir sind die StringUtils und verwandten Klassen bekannt, die Sie entweder selbst implementieren oder über das Web suchen müssen, um nur dieses Verhalten (4 Codezeilen) zu implementieren sind dazu bereit.

wleao
quelle
9
Diese Frage zu Stack Overflow könnte Sie interessieren: stackoverflow.com/questions/2068804/why-is-string-final-in-java
Thomas Owens
Ebenso, was würde passieren, wenn java.io.Filenicht final. Oh, das ist es nicht. Bugger.
Tom Hawtin - Tackline
1
Das Ende der westlichen Zivilisation, wie wir sie kennen.
Tulains Córdova

Antworten:

22

Der Hauptgrund liegt in der Geschwindigkeit: finalKlassen können nicht erweitert werden, wodurch die JIT alle Arten von Optimierungen beim Umgang mit Zeichenfolgen durchführen kann - es ist nie erforderlich, nach überschriebenen Methoden zu suchen.

Ein weiterer Grund ist die Thread-Sicherheit: Unveränderliche Dateien sind immer thread-sicher, da ein Thread sie vollständig erstellen muss, bevor sie an eine andere Person übergeben werden können - und nach dem Erstellen können sie nicht mehr geändert werden.

Auch die Erfinder der Java-Laufzeit wollten immer auf der Seite der Sicherheit irren. Wenn Sie String erweitern können (was ich in Groovy oft mache, weil es so praktisch ist), können Sie eine ganze Dose Würmer öffnen, wenn Sie nicht wissen, was Sie tun.

Aaron Digulla
quelle
2
Das ist eine falsche Antwort. Abgesehen von der für die JVM-Spezifikation erforderlichen Überprüfung dient HotSpot nur finalzur schnellen Überprüfung, ob eine Methode beim Kompilieren von Code nicht überschrieben wird.
Tom Hawtin - Tackline
1
@ TomHawtin-tackline: Referenzen?
Aaron Digulla
1
Sie scheinen zu implizieren, dass Unveränderlichkeit und Endgültigkeit irgendwie zusammenhängen. "Ein weiterer Grund ist die Thread-Sicherheit: Unveränderliche Dateien sind immer thread-sicher." Der Grund, warum ein Objekt unveränderlich ist oder nicht, liegt nicht darin, dass es endgültig ist oder nicht.
Tulains Córdova
1
Darüber hinaus ist die String - Klasse ist unveränderlich unabhängig von dem finalSchlüsselwort in der Klasse. Das darin enthaltene Zeichenarray ist final und daher unveränderlich. Wenn Sie die Klasse selbst final machen, wird die Schleife geschlossen, wenn @abl erwähnt wird, da eine veränderbare Unterklasse nicht eingeführt werden kann.
1
@Snowman Um genau zu sein, macht das Zeichenarray, das final ist, nur die Arrayreferenz unveränderlich, nicht den Inhalt des Arrays.
Michał Kosmulski
10

Es gibt einen weiteren Grund, warum Javas StringKlasse endgültig sein muss: In einigen Szenarien ist dies für die Sicherheit wichtig. Wenn die StringKlasse nicht endgültig wäre, wäre der folgende Code für subtile Angriffe eines böswilligen Aufrufers anfällig:

 public String makeSafeLink(String url, String text) {
     if (!url.startsWith("http:") && !url.startsWith("https:"))
         throw SecurityException("only http/https URLs are allowed");
     return "<a href=\"" + escape(url) + "\">" + escape(text) + "</a>";
 }

Der Angriff: Ein böswilliger Aufrufer könnte eine Unterklasse von String erstellen EvilString, in der EvilString.startsWith()immer true zurückgegeben wird, der Wert von EvilStringjedoch böse ist (z javascript:alert('xss'). B. ). Aufgrund der Unterklasse würde dies der Sicherheitsüberprüfung entgehen. Dieser Angriff wird als TOCTTOU-Sicherheitsanfälligkeit (Time-of-Check-to-Time-of-Use) bezeichnet: zwischen dem Zeitpunkt, zu dem die Überprüfung durchgeführt wurde (die URL beginnt mit http / https) und dem Zeitpunkt, zu dem der Wert verwendet wird (um das HTML-Snippet zu konstruieren), kann sich der effektive Wert ändern. Wenn dies Stringnicht endgültig wäre, wären die Risiken von TOCTTOU allgegenwärtig.

Wenn dies Stringnicht der Fall ist, wird es schwierig, sicheren Code zu schreiben, wenn Sie Ihrem Anrufer nicht vertrauen können. Dies ist natürlich genau die Position, in der sich die Java-Bibliotheken befinden: Sie werden möglicherweise von nicht vertrauenswürdigen Applets aufgerufen, sodass sie es nicht wagen, ihrem Aufrufer zu vertrauen. Dies bedeutet, dass es unangemessen schwierig wäre, sicheren Bibliothekscode zu schreiben, wenn er Stringnicht endgültig wäre.

DW
quelle
Natürlich ist es immer noch möglich, diese TOCTTOU-Vuln durch Reflektion zu entfernen, um das char[]Innere der Saite zu modifizieren , aber es erfordert etwas Glück beim Timing.
Peter Taylor
2
@ Peter Taylor, es ist komplizierter. Sie können Reflection nur verwenden, um auf private Variablen zuzugreifen, wenn der SecurityManager Ihnen die Berechtigung dazu erteilt. Applets und anderer nicht vertrauenswürdiger Code erhalten diese Berechtigung nicht und können daher keine Reflektion verwenden, um das Private char[]in der Zeichenfolge zu ändern . Daher können sie die TOCTTOU-Sicherheitsanfälligkeit nicht ausnutzen.
DW
Ich weiß, aber das lässt immer noch Anwendungen und signierte Applets übrig - und wie viele Leute werden tatsächlich vom Warndialog für ein selbstsigniertes Applet abgeschreckt?
Peter Taylor
2
@ Peter, das ist nicht der Punkt. Der Punkt ist, dass das Schreiben von vertrauenswürdigen Java-Bibliotheken sehr viel schwieriger sein würde und es viel mehr gemeldete Sicherheitslücken in Java-Bibliotheken geben würde, wenn dies Stringnicht endgültig wäre. Solange dieses Bedrohungsmodell relevant ist, ist es wichtig, sich dagegen zu verteidigen. Solange es für Applets und anderen nicht vertrauenswürdigen Code wichtig ist, ist dies wahrscheinlich genug, um die Stringendgültige Erstellung zu rechtfertigen , auch wenn es für vertrauenswürdige Anwendungen nicht benötigt wird. (In jedem Fall können vertrauenswürdige Anwendungen bereits alle Sicherheitsmaßnahmen umgehen, unabhängig davon, ob sie endgültig sind.)
DW
"Der Angriff: Ein böswilliger Aufrufer könnte eine Unterklasse von String, EvilString, erstellen, in der EvilString.startsWith () immer true zurückgibt ..." - nicht, wenn startsWith () final ist.
Erik
4

Wenn nicht, wären Multithread-Java-Apps ein Durcheinander (noch schlimmer als das, was tatsächlich ist).

IMHO Der Hauptvorteil von finalen Strings (unveränderlich) ist, dass sie inhärent threadsicher sind: Sie erfordern keine Synchronisation (das Schreiben dieses Codes ist manchmal ziemlich trivial, aber mehr als weniger weit davon entfernt). Wenn die veränderlich wären, wäre es sehr schwierig, Dinge wie Multithread-Windows-Toolkits zu gewährleisten, und diese Dinge sind unbedingt erforderlich.

Randolf Rincón Fadul
quelle
3
finalKlassen haben nichts mit der Wandlungsfähigkeit der Klasse zu tun.
Diese Antwort geht nicht auf die Frage ein. -1
FUZxxl
3
Jarrod Roberson: Wenn die Klasse nicht endgültig wäre, könnten Sie mithilfe der Vererbung veränderbare Zeichenfolgen erstellen.
ysdx
@JarrodRoberson endlich merkt jemand den Fehler. Die akzeptierte Antwort impliziert auch, dass das Endergebnis unveränderlich ist.
Tulains Córdova
1

Ein weiterer Vorteil Stringder Finalität besteht darin, dass vorhersehbare Ergebnisse wie Unveränderlichkeit gewährleistet sind. StringObwohl es sich um eine Klasse handelt, soll sie in einigen Szenarien wie ein Werttyp behandelt werden (der Compiler unterstützt sie sogar über String blah = "test"). Wenn Sie also eine Zeichenfolge mit einem bestimmten Wert erstellen, erwarten Sie, dass diese ein bestimmtes Verhalten aufweist, das an eine beliebige Zeichenfolge gebunden ist, ähnlich den Erwartungen, die Sie an Ganzzahlen haben würden.

Denken Sie darüber nach: Was ist, wenn Sie java.lang.Stringdie equalsoder hashCode-Methoden unterklassifizieren und überschreiben ? Plötzlich konnten sich zwei Strings "test" nicht mehr gleichen.

Stellen Sie sich das Chaos vor, wenn ich das könnte:

public class Password extends String {
    public Password(String text) {
        super(text);
    }

    public boolean equals(Object o) {
        return true;
    }
}

eine andere Klasse:

public boolean isRightPassword(String suppliedString) {
    return suppliedString != null && suppliedString.equals("secret");
}

public void login(String username, String password) {
    return isRightUsername(username) && isRightPassword(password);
}

public void loginTest() {
    boolean success = login("testuser", new Password("wrongpassword"));
    // success is true, despite the password being wrong
}
Brandon
quelle
1

Hintergrund

Es gibt drei Stellen, an denen final in Java angezeigt werden kann:

Das Finalisieren einer Klasse verhindert alle Unterklassen der Klasse. Das Finalisieren einer Methode verhindert, dass Unterklassen der Methode diese überschreiben. Das Finalisieren eines Feldes verhindert, dass es später geändert wird.

Missverständnisse

Es gibt Optimierungen, die sich auf endgültige Methoden und Felder beziehen.

Eine abschließende Methode erleichtert HotSpot die Optimierung über Inlining. HotSpot führt dies jedoch aus, auch wenn die Methode nicht endgültig ist, da davon ausgegangen wird, dass sie bis zum Beweis des Gegenteils nicht überschrieben wurde. Mehr dazu auf SO

Eine endgültige Variable kann aggressiv optimiert werden, und mehr dazu im JLS-Abschnitt 17.5.3 .

Mit diesem Verständnis sollte man sich jedoch darüber im Klaren sein, dass es bei keiner dieser Optimierungen darum geht, eine Klasse endgültig zu machen. Es gibt keinen Leistungsgewinn, wenn eine Klasse zum Finale wird.

Der letzte Aspekt einer Klasse hat auch nichts mit Unveränderlichkeit zu tun. Man kann eine unveränderliche Klasse (wie BigInteger ) haben, die nicht final ist, oder eine Klasse, die veränderlich und final ist (wie StringBuilder ). Die Entscheidung, ob eine Klasse endgültig sein soll, ist eine Frage des Designs.

Endgültiges Design

Zeichenfolgen gehören zu den am häufigsten verwendeten Datentypen. Sie werden als Schlüssel für Karten gefunden, sie speichern Benutzernamen und Kennwörter, sie sind das, was Sie über eine Tastatur oder ein Feld auf einer Webseite einlesen. Saiten sind überall .

Karten

Als Erstes müssen Sie berücksichtigen, was passieren würde, wenn Sie eine Unterklasse für String erstellen könnten, indem Sie erkennen, dass jemand eine veränderbare String-Klasse erstellen könnte, die ansonsten anscheinend ein String wäre. Dies würde Maps überall durcheinander bringen.

Betrachten Sie diesen hypothetischen Code:

Map t = new TreeMap<String, Integer>();
Map h = new HashMap<String, Integer>();
MyString one = new MyString("one");
MyString two = new MyString("two");

t.put(one, 1); h.put(one, 1);
t.put(two, 2); h.put(two, 2);

one.prepend("z");

Dies ist ein Problem bei der Verwendung eines veränderlichen Schlüssels im Allgemeinen mit einer Karte, aber die Sache, die ich versuche, dort anzukommen, ist, dass plötzlich eine Reihe von Dingen an der Karte kaputt gehen. Der Eintrag befindet sich nicht mehr an der richtigen Stelle in der Karte. In einer HashMap hat sich der Hash-Wert geändert (sollte sich geändert haben) und ist daher nicht mehr am richtigen Eintrag. In der TreeMap ist der Baum nun gebrochen, weil sich einer der Knoten auf der falschen Seite befindet.

Da die Verwendung eines Strings für diese Schlüssel so verbreitet ist, sollte dieses Verhalten verhindert werden, indem der String als final definiert wird.

Vielleicht interessiert Sie das Lesen Warum ist String in Java unveränderlich? Hier erfahren Sie mehr über die Unveränderlichkeit von Strings.

Schändliche Saiten

Es gibt eine Reihe von verschiedenen schändlichen Optionen für Strings. Überlegen Sie, ob ich einen String erstellt habe, der beim Aufruf von equal immer true zurückgibt ... und diesen an eine Kennwortprüfung weiterleitet? Oder wurde es so gemacht, dass Zuweisungen an MyString eine Kopie des Strings an eine E-Mail-Adresse senden würden?

Dies sind sehr reale Möglichkeiten, wenn Sie die Möglichkeit haben, String in Unterklassen zu unterteilen.

Java.lang String-Optimierungen

Während ich vorhin erwähnte, macht das Finale String nicht schneller. Die String-Klasse (und andere Klassen in java.lang) verwenden jedoch häufig den Schutz von Feldern und Methoden auf Paketebene, damit andere java.langKlassen mit den Interna basteln können, anstatt die öffentliche API für String ständig zu durchlaufen. Funktionen wie getChars ohne Bereichsprüfung oder lastIndexOf , das von StringBuffer verwendet wird, oder der Konstruktor , der das zugrunde liegende Array gemeinsam nutzt (beachten Sie, dass dies ein Java 6-Element ist, das aufgrund von Speicherproblemen geändert wurde).

Wenn jemand eine Unterklasse von String erstellt hätte, wäre er nicht in der Lage, diese Optimierungen zu teilen (es sei denn, es war auch Teil von java.lang, aber das ist ein versiegeltes Paket ).

Es ist schwieriger, etwas für die Erweiterung zu entwerfen

Es ist schwierig, etwas zu entwerfen, das erweiterbar ist . Dies bedeutet, dass Sie Teile Ihrer Interna freigeben müssen, damit etwas anderes geändert werden kann.

Bei einem erweiterbaren String konnte der Speicherverlust nicht behoben werden. Diese Teile müssten Unterklassen ausgesetzt worden sein, und das Ändern dieses Codes würde dann bedeuten, dass die Unterklassen beschädigt würden.

Java ist stolz auf seine Abwärtskompatibilität. Indem man Kernklassen für Erweiterungen öffnet, verliert man die Fähigkeit, Probleme zu beheben, während die Kompatibilität mit Unterklassen von Drittanbietern erhalten bleibt.

Checkstyle hat eine Regel namens "DesignForExtension", die erzwingt (was mich beim Schreiben von internem Code wirklich frustriert), dass jede Klasse entweder:

  • Abstrakt
  • Finale
  • Leere Implementierung

Der Grund dafür ist:

Dieser API-Entwurfsstil schützt Superklassen vor dem Aufbrechen durch Unterklassen. Der Nachteil ist, dass Unterklassen in ihrer Flexibilität begrenzt sind, insbesondere können sie die Ausführung von Code in der Superklasse nicht verhindern. Dies bedeutet jedoch auch, dass Unterklassen den Status der Superklasse nicht beschädigen können, indem sie den Aufruf der Supermethode vergessen.

Das Ermöglichen der Erweiterung von Implementierungsklassen bedeutet, dass die Unterklassen möglicherweise den Status der Klasse, auf der sie basieren, verfälschen und so festlegen können, dass verschiedene Garantien, die die Superklasse gibt, ungültig sind. Für etwas so komplex wie String, ist es fast sicher , dass ein Teil davon zu ändern wird etwas brechen.

Entwickler hurbis

Es gehört dazu, Entwickler zu sein. Berücksichtigen Sie die Wahrscheinlichkeit, dass jeder Entwickler eine eigene String-Unterklasse mit einer eigenen Sammlung von Dienstprogrammen erstellt . Jetzt können diese Unterklassen nicht mehr frei einander zugeordnet werden.

WleaoString foo = new WleaoString("foo");
MichaelTString bar = foo; // This doesn't work.

Dieser Weg führt zum Wahnsinn. Überall in String umwandeln und prüfen, ob der String eine Instanz Ihrer String-Klasse ist oder nicht. Dann einen neuen String basierend auf diesem erstellen und ... einfach nein. Nicht.

Ich bin mir sicher, dass Sie eine gute String-Klasse schreiben können ... aber überlassen Sie es diesen verrückten Leuten, die C ++ schreiben und sich mit std::stringund char*und etwas von Boost und SString und dem Rest auseinandersetzen müssen , das Schreiben mehrerer String-Implementierungen .

Java String Magie

Es gibt verschiedene magische Dinge, die Java mit Strings macht. Dies erleichtert einem Programmierer den Umgang mit der Sprache, führt jedoch zu einigen Inkonsistenzen in der Sprache. Das Zulassen von Unterklassen in String erfordert einige sehr wichtige Überlegungen zum Umgang mit diesen magischen Dingen:

  • String-Literale (JLS 3.10.5 )

    Mit Code, der Folgendes ermöglicht:

    String foo = "foo";

    Dies ist nicht zu verwechseln mit Boxing der numerischen Typen wie Integer. Sie können nicht tun 1.toString(), aber Sie können tun "foo".concat(bar).

  • Der +Betreiber (JLS 15.18.1 )

    Kein anderer Referenztyp in Java erlaubt die Verwendung eines Operators. Die Zeichenfolge ist etwas Besonderes. Der String - Verkettungsoperator arbeitet auch an der Compiler - Ebene , so dass "foo" + "bar"wird , "foobar"wenn es kompiliert wird , anstatt zur Laufzeit.

  • String-Konvertierung (JLS 5.1.11 )

    Alle Objekte können in Strings konvertiert werden, indem sie in einem String-Kontext verwendet werden.

  • String Interning ( JavaDoc )

    Die String-Klasse hat Zugriff auf den Pool von Strings, mit dem kanonische Darstellungen des Objekts möglich sind, das beim Kompilierungstyp mit den String-Literalen gefüllt wird.

Das Zulassen einer Unterklasse von String würde bedeuten, dass diese Bits mit dem String, die das Programmieren erleichtern, sehr schwierig oder unmöglich sind, wenn andere String-Typen möglich sind.

Gemeinschaft
quelle
0

Wenn String nicht endgültig wäre, würde jeder Programmierer-Werkzeugkasten einen eigenen "String mit nur ein paar netten Hilfsmethoden" enthalten. Jede davon wäre mit allen anderen Werkzeugkisten nicht kompatibel.

Ich hielt es für eine dumme Einschränkung. Heute bin ich überzeugt, dass dies eine sehr gute Entscheidung war. Es hält Streicher für Streicher.


quelle
finalin Java ist nicht dasselbe wie finalin C #. In diesem Zusammenhang ist es dasselbe wie in C # readonly. Siehe stackoverflow.com/questions/1327544/…
Arseni Mourzenko
9
@MainMa: Eigentlich scheint es in diesem Zusammenhang dasselbe zu sein wie bei C # public final class String.
Konfigurator