Que se passerait-il vraiment si java.lang.String n'était pas définitif?

16

Je suis un développeur Java de longue date et enfin, après la majeure, j'ai le temps de l'étudier décemment afin de passer l'examen de certification ... Une chose qui m'a toujours dérangé, c'est que String est "final". Je le comprends quand une lecture sur les problèmes de sécurité et les choses connexes ... Mais, sérieusement, quelqu'un a-t-il un vrai exemple de cela?

Par exemple, que se passerait-il si String n'était pas définitif? Comme si ce n'était pas en Ruby. Je n'ai entendu aucune plainte de la part de la communauté Ruby ... Et je connais les StringUtils et les classes connexes que vous devez soit implémenter vous-même, soit rechercher sur le Web pour simplement implémenter ce comportement (4 lignes de code) vous suis disposé à.

wleao
la source
9
Vous pourriez être intéressé par cette question sur Stack Overflow: stackoverflow.com/questions/2068804/why-is-string-final-in-java
Thomas Owens
De même, que se passerait-il si ce java.io.Filen'était pas le cas final. Oh, ça ne l'est pas. Bugger.
Tom Hawtin - tackline
1
La fin de la civilisation occidentale telle que nous la connaissons.
Tulains Córdova

Réponses:

22

La raison principale est la vitesse: les finalclasses ne peuvent pas être étendues, ce qui a permis au JIT de faire toutes sortes d'optimisations lors de la gestion des chaînes - il n'est jamais nécessaire de vérifier les méthodes substituées.

Une autre raison est la sécurité des threads: les Immutables sont toujours thread-safe car un thread doit les construire complètement avant de pouvoir être transmis à quelqu'un d'autre - et après la construction, ils ne peuvent plus être modifiés.

De plus, les inventeurs du runtime Java ont toujours voulu pécher par excès de sécurité. Pouvoir étendre String (quelque chose que je fais souvent dans Groovy parce que c'est si pratique) peut ouvrir toute une boîte de vers si vous ne savez pas ce que vous faites.

Aaron Digulla
la source
2
C'est une réponse incorrecte. À part la vérification requise par la spécification JVM, HotSpot n'utilise finalque pour vérifier rapidement qu'une méthode n'est pas remplacée lors de la compilation du code.
Tom Hawtin - tackline
1
@ TomHawtin-tackline: Références?
Aaron Digulla
1
Vous semblez impliquer que l'immuabilité et le fait d'être final sont en quelque sorte liés. "Une autre raison est la sécurité des threads: les Immutables sont toujours thread-safe b ...". La raison pour laquelle un objet est immuable ou non n'est pas parce qu'il est ou n'est pas définitif.
Tulains Córdova
1
De plus, la classe String est immuable quel que soit le finalmot clé de la classe. Le tableau de caractères qu'il contient est final, ce qui le rend immuable. Rendre la classe elle-même finale ferme la boucle comme le mentionne @abl car une sous-classe mutable ne peut pas être introduite.
1
@Snowman Pour être précis, le tableau de caractères final ne fait qu'immuable la référence du tableau, pas le contenu du tableau.
Michał Kosmulski
10

Il y a une autre raison pour laquelle la Stringclasse Java doit être définitive: elle est importante pour la sécurité dans certains scénarios. Si la Stringclasse n'était pas définitive, le code suivant serait vulnérable aux attaques subtiles d'un appelant malveillant:

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

L'attaque: un appelant malveillant pourrait créer une sous-classe de String, EvilStringEvilString.startsWith()renvoie toujours true mais où la valeur de EvilStringest quelque chose de mal (par exemple, javascript:alert('xss')). En raison du sous-classement, cela échapperait au contrôle de sécurité. Cette attaque est connue sous le nom de vulnérabilité TOCTTOU (time-of-check-to-time-of-use): entre le moment où le contrôle est effectué (que l'url commence par http / https) et le moment où la valeur est utilisée. (pour construire l'extrait html), la valeur effective peut changer. S'il Stringn'était pas définitif, les risques TOCTTOU seraient omniprésents.

Donc, si ce Stringn'est pas définitif, il devient difficile d'écrire du code sécurisé si vous ne pouvez pas faire confiance à votre appelant. Bien sûr, c'est exactement la position dans laquelle se trouvent les bibliothèques Java: elles peuvent être invoquées par des applets non fiables, de sorte qu'elles n'osent pas faire confiance à leur appelant. Cela signifie qu'il serait déraisonnablement difficile d'écrire du code de bibliothèque sécurisé, s'il Stringn'était pas définitif.

DW
la source
Bien sûr, il est toujours possible de retirer cette vulve TOCTTOU en utilisant la réflexion pour modifier l' char[]intérieur de la chaîne, mais cela nécessite de la chance dans le timing.
Peter Taylor
2
@Peter Taylor, c'est plus compliqué que ça. Vous ne pouvez utiliser la réflexion pour accéder à des variables privées que si le SecurityManager vous y autorise. Les applets et autres codes non approuvés ne reçoivent pas cette autorisation et ne peuvent donc pas utiliser la réflexion pour modifier le privé char[]à l'intérieur de la chaîne; par conséquent, ils ne peuvent pas exploiter la vulnérabilité TOCTTOU.
DW
Je sais, mais cela laisse encore des applications et des applets signées - et combien de personnes sont réellement effrayées par la boîte de dialogue d'avertissement pour une applet auto-signée?
Peter Taylor
2
@Peter, ce n'est pas le but. Le fait est que l'écriture de bibliothèques Java de confiance serait beaucoup plus difficile et qu'il y aurait beaucoup plus de vulnérabilités signalées dans les bibliothèques Java, si elles Stringn'étaient pas définitives. Aussi longtemps que ce modèle de menace est pertinent, il devient important de se défendre contre. Tant qu'il est important pour les applets et autres codes non fiables, cela est sans doute suffisant pour justifier la finalisation String, même s'il n'est pas nécessaire pour les applications de confiance. (Dans tous les cas, les applications de confiance peuvent déjà contourner toutes les mesures de sécurité, qu'elles soient finales ou non.)
DW
"L'attaque: un appelant malveillant pourrait créer une sous-classe de String, EvilString, où EvilString.startsWith () renvoie toujours true ..." - pas si startWith () est final.
Erik
4

Sinon, les applications java multithread seraient un gâchis (encore pire que ce qui est réellement).

IMHO Le principal avantage des chaînes finales (immuables) est qu'elles sont intrinsèquement thread-safe: elles ne nécessitent aucune synchronisation (l'écriture de ce code est parfois assez triviale mais plus que moins éloignée de cela). Si le était mutable, garantir des choses comme les boîtes à outils Windows multithread serait très difficile, et ces choses sont strictement nécessaires.

Randolf Rincón Fadul
la source
3
finalles classes n'ont rien à voir avec la mutabilité de la classe.
Cette réponse ne répond pas à la question. -1
FUZxxl
3
Jarrod Roberson: si la classe n'était pas finale, vous pourriez créer des chaînes mutables en utilisant l'héritage.
ysdx
@JarrodRoberson enfin quelqu'un remarque l'erreur. La réponse acceptée implique également que final est égal à immuable.
Tulains Córdova
1

Un autre avantage d' Stringêtre final est qu'il garantit des résultats prévisibles, tout comme l'immuabilité. String, bien qu'une classe, est destinée à être traitée dans quelques scénarios, comme un type de valeur (le compilateur le prend même en charge via String blah = "test"). Par conséquent, si vous créez une chaîne avec une certaine valeur, vous vous attendez à ce qu'elle ait un certain comportement hérité de toute chaîne, semblable aux attentes que vous auriez avec des entiers.

Réfléchissez à ceci: que se passe-t-il si vous sous-classe java.lang.Stringet surfait sur les méthodes equalsou hashCode? Soudain, deux «tests» de cordes ne pouvaient plus s'égaler.

Imaginez le chaos si je pouvais faire ça:

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

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

une autre classe:

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
la source
1

Contexte

Il y a trois endroits où final peut apparaître en Java:

Faire une finale de classe empêche tout sous-classement de la classe. La finalisation d'une méthode empêche les sous-classes de la méthode de la remplacer. Rendre un champ final empêche qu'il ne soit modifié ultérieurement.

Idées fausses

Des optimisations se produisent autour des méthodes et des champs finaux.

Une dernière méthode facilite l'optimisation de HotSpot via l'inlining. Cependant, HotSpot le fait même si la méthode n'est pas définitive car elle fonctionne en supposant qu'elle n'a pas été remplacée jusqu'à preuve du contraire. En savoir plus sur SO

Une variable finale peut être optimisée de manière agressive, et plus à ce sujet peut être lue dans la section JLS 17.5.3 .

Cependant, avec cette compréhension, il faut savoir qu'aucune de ces optimisations ne concerne la finalisation d'une classe . Il n'y a aucun gain de performance en faisant une finale de classe.

L'aspect final d'une classe n'a rien à voir non plus avec l'immuabilité. On peut avoir une classe immuable (telle que BigInteger ) qui n'est pas finale, ou une classe qui est mutable et finale (telle que StringBuilder ). La décision de savoir si une classe doit être finale est une question de conception.

Conception finale

Les chaînes sont l'un des types de données les plus utilisés. Ils se trouvent comme clés de cartes, ils stockent des noms d'utilisateur et des mots de passe, ils sont ce que vous lisez à partir d'un clavier ou d'un champ sur une page Web. Les cordes sont partout .

Plans

La première chose à considérer de ce qui se passerait si vous pouviez sous-classer String est de réaliser que quelqu'un pourrait construire une classe String mutable qui semblerait autrement être une String. Cela gâcherait Maps partout.

Considérez ce code hypothétique:

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

C'est un problème avec l'utilisation d'une clé mutable en général avec une carte, mais ce que j'essaie de comprendre, c'est que soudainement un certain nombre de choses à propos de la rupture de la carte. L'entrée n'est plus au bon endroit sur la carte. Dans un HashMap, la valeur de hachage a (aurait dû) changé et donc ce n'est plus à la bonne entrée. Dans TreeMap, l'arborescence est maintenant cassée car l'un des nœuds est du mauvais côté.

Étant donné que l'utilisation d'une chaîne pour ces clés est si courante, ce comportement doit être évité en rendant la chaîne finale.

Vous pourriez être intéressé à lire Pourquoi String est-il immuable en Java? pour en savoir plus sur la nature immuable de Strings.

Cordes néfastes

Il existe un certain nombre d'options néfastes pour les chaînes. Considérez si j'ai fait une chaîne qui retournait toujours vrai quand égal était appelé ... et passé cela dans une vérification de mot de passe? Ou fait en sorte que les affectations à MyString envoient une copie de la chaîne à une adresse e-mail?

Ce sont des possibilités très réelles lorsque vous avez la possibilité de sous-classer String.

Optimisations de chaîne Java.lang

Alors qu'avant, j'ai mentionné que la finale ne rend pas la chaîne plus rapide. Cependant, la classe String (et les autres classes java.lang) utilise fréquemment la protection au niveau du package des champs et des méthodes pour permettre aux autres java.langclasses de pouvoir bricoler avec les internes plutôt que de passer par l'API publique pour String tout le temps. Fonctions comme getChars sans vérification de plage ou lastIndexOf qui est utilisé par StringBuffer ou le constructeur qui partage le tableau sous-jacent (notez que c'est une chose Java 6 qui a été modifiée en raison de problèmes de mémoire).

Si quelqu'un créait une sous-classe de String, il ne serait pas en mesure de partager ces optimisations (sauf si cela en faisait partie java.langaussi, mais c'est un package scellé ).

Il est plus difficile de concevoir quelque chose pour l'extension

Concevoir quelque chose pour être extensible est difficile . Cela signifie que vous devez exposer des parties de vos composants internes pour que quelque chose d'autre puisse être modifié.

Une fuite de mémoire ne pouvait pas avoir sa fuite de mémoire fixée. Ces parties devraient avoir été exposées à des sous-classes et changer ce code signifierait alors que les sous-classes se briseraient.

Java se targue de la compatibilité descendante et en ouvrant les classes de base à l'extension, on perd une partie de cette capacité à réparer les choses tout en conservant la calculabilité avec les sous-classes tierces.

Checkstyle a une règle qu'il applique (qui me frustre vraiment lors de l'écriture de code interne) appelée "DesignForExtension" qui impose que chaque classe soit:

  • Abstrait
  • Final
  • Implémentation vide

Le rationnel pour lequel est:

Ce style de conception d'API protège les superclasses contre les cassures de sous-classes. L'inconvénient est que les sous-classes sont limitées dans leur flexibilité, en particulier, elles ne peuvent pas empêcher l'exécution de code dans la superclasse, mais cela signifie également que les sous-classes ne peuvent pas corrompre l'état de la superclasse en oubliant d'appeler la méthode super.

Autoriser l'extension des classes d'implémentation signifie que les sous-classes peuvent éventuellement corrompre l'état de la classe à partir de laquelle elles sont basées et faire en sorte que diverses garanties que la superclasse donne ne soient pas valides. Pour quelque chose d' aussi complexe que la chaîne, il est presque certain que le changement de partie se casser quelque chose.

Développeur hurbis

Sa part d'être développeur. Considérez la probabilité que chaque développeur crée sa propre sous-classe String avec sa propre collection d' utilitaires . Mais maintenant, ces sous-classes ne peuvent pas être librement attribuées les unes aux autres.

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

Cette voie mène à la folie. Casting à String partout et vérifier si la String est une instance de votre classe String ou si non, créer une nouvelle String basée sur celle-ci et ... juste, non. Non.

Je suis sûr que vous pouvez écrire une bonne classe String ... mais laisser l' écriture des implémentations de chaînes multiples à ces fous qui écrivent C ++ et doivent traiter std::stringet char*et quelque chose de stimuler et sChaîne , et tout le reste .

Java String Magic

Il y a plusieurs choses magiques que Java fait avec les chaînes. Ceux-ci facilitent la tâche d'un programmeur mais introduisent certaines incohérences dans le langage. Autoriser des sous-classes sur String nécessiterait une réflexion très importante sur la façon de gérer ces choses magiques:

  • Littéraux de chaîne (JLS 3.10.5 )

    Avoir du code qui permet de faire:

    String foo = "foo";

    Cela ne doit pas être confondu avec la boxe des types numériques comme Integer. Vous ne pouvez pas le faire 1.toString(), mais vous pouvez le faire "foo".concat(bar).

  • L' +opérateur (JLS 15.18.1 )

    Aucun autre type de référence en Java ne permet d'utiliser un opérateur dessus. La chaîne est spéciale. L'opérateur de concaténation de chaînes fonctionne également au niveau du compilateur de sorte que cela "foo" + "bar"se produit "foobar"lors de la compilation plutôt qu'au moment de l'exécution.

  • Conversion de chaînes (JLS 5.1.11 )

    Tous les objets peuvent être convertis en chaînes simplement en les utilisant dans un contexte de chaîne.

  • Interning de chaînes ( JavaDoc )

    La classe String a accès au pool de chaînes qui lui permet d'avoir des représentations canoniques de l'objet qui est rempli au type de compilation avec les littéraux String.

Autoriser une sous-classe de chaîne signifierait que ces bits avec la chaîne qui facilitent la programmation deviendraient très difficiles ou impossibles à faire lorsque d'autres types de chaînes sont possibles.

Communauté
la source
0

Si String n'était pas définitif, chaque coffre à outils de programmeur contiendrait son propre "String avec juste quelques belles méthodes d'assistance". Chacun d'eux serait incompatible avec tous les autres coffres à outils.

J'ai considéré que c'était une restriction idiote. Aujourd'hui, je suis convaincu que cette décision a été très judicieuse. Il garde les cordes comme des cordes.


la source
finalen Java n'est pas la même chose qu'en finalC #. Dans ce contexte, c'est la même chose que C # readonly. Voir stackoverflow.com/questions/1327544/…
Arseni Mourzenko
9
@MainMa: En fait, dans ce contexte, il semble que ce soit le même que C # scellé, comme dans public final class String.
configurateur