web-dev-qa-db-fra.com

Pourquoi String est-il immuable en Java?

Je ne pouvais pas en comprendre la raison. J'utilise toujours la classe String comme les autres développeurs, mais lorsque j'en modifie la valeur, une nouvelle instance de String est créée.

Quelle pourrait être la raison de l'immuabilité pour la classe String en Java?

Je sais qu'il existe des alternatives comme StringBuffer ou StringBuilder. C'est juste de la curiosité.

80
yfklon

Accès simultané

Java a été défini dès le départ avec des considérations de concurrence. Comme souvent mentionné, les mutables partagés sont problématiques. Une chose peut en changer une autre derrière le dos d'un autre thread sans que ce thread en soit conscient.

Il existe un hôte de bogues C++ multithread qui ont surgi à cause d'une chaîne partagée - où un module pensait qu'il était sûr de changer lorsqu'un autre module dans le code lui avait enregistré un pointeur et s'attendait à ce qu'il reste le même.

La "solution" à cela est que chaque classe crée une copie défensive des objets mutables qui lui sont transmis. Pour les chaînes mutables, c'est O(n) pour faire la copie. Pour les chaînes immuables, faire une copie est O(1) parce que ce n'est pas une copie, c'est le même objet qui ne peut pas changer.

Dans un environnement multithread, les objets immuables peuvent toujours être partagés en toute sécurité entre eux. Cela conduit à une réduction globale de l'utilisation de la mémoire et améliore la mise en cache de la mémoire.

Sécurité

Plusieurs fois, les chaînes sont transmises comme arguments aux constructeurs - les connexions réseau et les protocoles sont les deux qui viennent le plus facilement à l'esprit. Pouvoir changer cela à une heure indéterminée plus tard dans l'exécution peut entraîner des problèmes de sécurité (la fonction pensait qu'elle se connectait à une machine, mais a été détournée vers une autre, mais tout dans l'objet semble être connecté à la première ... c'est même la même chaîne).

Java permet d'utiliser la réflexion - et les paramètres pour cela sont des chaînes. Le danger que l'on passe une chaîne qui peut être modifiée en cours de route vers une autre méthode qui se reflète. C'est très mauvais.

Clés du hachage

La table de hachage est l'une des structures de données les plus utilisées. Les clés de la structure de données sont très souvent des chaînes. Le fait d'avoir des chaînes immuables signifie que (comme ci-dessus) la table de hachage n'a pas besoin de faire une copie de la clé de hachage à chaque fois. Si les chaînes étaient mutables et que la table de hachage ne le faisait pas, il serait possible que quelque chose change la clé de hachage à distance.

La façon dont l'Object dans Java fonctionne, est que tout a une clé de hachage (accessible via la méthode hashCode ()). Avoir une chaîne immuable signifie que le hashCode peut être mis en cache. Considérant la fréquence à laquelle les chaînes sont utilisées comme clés d'un hachage, ce qui améliore considérablement les performances (plutôt que d'avoir à recalculer le code de hachage à chaque fois).

Sous-chaînes

En ayant la chaîne immuable, le tableau de caractères sous-jacent qui soutient la structure de données est également immuable. Cela permet certaines optimisations sur la méthode substring à faire (elles ne sont pas nécessairement effectuées - cela introduit également la possibilité de certaines la mémoire fuit aussi).

Si tu fais:

String foo = "smiles";
String bar = foo.substring(1,5);

La valeur de bar est 'mile'. Cependant, foo et bar peuvent être sauvegardés par le même tableau de caractères, ce qui réduit l'instanciation de plusieurs tableaux de caractères ou le copie - en utilisant simplement des points de début et de fin différents dans la chaîne.

 foo | | (0, 6) 
 V v 
 Sourit 
 ^ ^ 
 Bar | | (1, 5) 

Maintenant, l'inconvénient de cela (la fuite de mémoire) est que si quelqu'un avait une chaîne longue de 1k et prenait la sous-chaîne du premier et du deuxième caractère, il serait également soutenu par le tableau de caractères long de 1k. Ce tableau resterait en mémoire même si la chaîne d'origine qui avait une valeur de l'ensemble du tableau de caractères était récupérée.

On peut le voir dans String from JDK 6b14 (le code suivant provient d'une source GPL v2 et utilisé comme exemple)

   public String(char value[], int offset, int count) {
       if (offset < 0) {
           throw new StringIndexOutOfBoundsException(offset);
       }
       if (count < 0) {
           throw new StringIndexOutOfBoundsException(count);
       }
       // Note: offset or count might be near -1>>>1.
       if (offset > value.length - count) {
           throw new StringIndexOutOfBoundsException(offset + count);
       }
       this.offset = 0;
       this.count = count;
       this.value = Arrays.copyOfRange(value, offset, offset+count);
   }

   // Package private constructor which shares value array for speed.
   String(int offset, int count, char value[]) {
       this.value = value;
       this.offset = offset;
       this.count = count;
   }

   public String substring(int beginIndex, int endIndex) {
       if (beginIndex < 0) {
           throw new StringIndexOutOfBoundsException(beginIndex);
       }
       if (endIndex > count) {
           throw new StringIndexOutOfBoundsException(endIndex);
       }
       if (beginIndex > endIndex) {
           throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
       }
       return ((beginIndex == 0) && (endIndex == count)) ? this :
           new String(offset + beginIndex, endIndex - beginIndex, value);
   }

Notez comment la sous-chaîne utilise le constructeur String au niveau du package qui n'implique aucune copie du tableau et serait beaucoup plus rapide (au détriment de la possibilité de conserver quelques grands tableaux - sans pour autant dupliquer les grands tableaux).

Notez que le code ci-dessus est pour Java 1.6. La façon dont le constructeur de sous-chaîne est implémenté a été modifiée avec Java 1.7 comme indiqué dans Modifications apportées à Représentation interne de chaîne faite en Java 1.7.0_06 - le problème avec cette fuite de mémoire que j'ai mentionné ci-dessus. Java n'était probablement pas considéré comme étant un langage avec beaucoup de manipulation de chaînes et donc l'augmentation des performances pour une sous-chaîne était une bonne chose. Maintenant, avec d'énormes documents XML stockés dans des chaînes qui ne sont jamais collectées, cela devient un problème ... et donc le changement de String n'utilisant pas le même tableau sous-jacent avec une sous-chaîne, afin que le plus grand tableau de caractères puisse être collecté plus rapidement.

N'abusez pas de la pile

Un pourrait transmettre la valeur de la chaîne au lieu de la référence à la chaîne immuable pour éviter les problèmes de mutabilité. Cependant, avec de grandes chaînes, le passer sur la pile serait ... abusif pour le système (placer des documents xml entiers sous forme de chaînes sur la pile, puis les retirer ou continuer à les transmettre ...).

La possibilité de déduplication

Certes, ce n'était pas une motivation initiale pour expliquer pourquoi les cordes devraient être immuables, mais quand on regarde la raison pour laquelle les cordes immuables sont une bonne chose, c'est certainement quelque chose à considérer.

Quiconque a un peu travaillé avec Strings sait qu'il peut aspirer la mémoire. Cela est particulièrement vrai lorsque vous faites des choses comme extraire des données de bases de données qui restent pendant un certain temps. Plusieurs fois avec ces piqûres, ce sont la même chaîne encore et encore (une fois pour chaque ligne).

De nombreuses applications à grande échelle Java sont actuellement goulot d'étranglement dans la mémoire. Les mesures ont montré qu'environ 25% de l'ensemble de données en direct Java tas dans ces types d'applications est consommés par les objets String. De plus, environ la moitié de ces objets String sont des doublons, ce qui signifie que string1.equals (string2) est vrai. Le fait d'avoir des objets String en double sur le tas est, essentiellement, juste un gaspillage de mémoire. ...

Avec Java 8 update 20, JEP 192 (motivation citée ci-dessus) est mis en œuvre pour résoudre ce problème. Sans entrer dans les détails du fonctionnement de la déduplication des chaînes, il est essentiel que les chaînes elles-mêmes sont immuables. Vous ne pouvez pas dédupliquer les StringBuilders car ils peuvent changer et vous ne voulez pas que quelqu'un change quelque chose sous vous. Les chaînes immuables (liées à ce pool de chaînes) signifient que vous pouvez passer par et si vous en trouvez deux des chaînes identiques, vous pouvez pointer une référence de chaîne vers l'autre et laisser le garbage collector consommer la nouvelle non utilisée.

Autres langues

L'objectif C (qui précède Java) a NSString et NSMutableString.

C # et .NET ont fait les mêmes choix de conception, la chaîne par défaut étant immuable.

Lua les chaînes sont également immuables.

Python également.

Historiquement, LISP, Scheme, Smalltalk all intern the string et l'ont donc immuable. Les langages dynamiques plus modernes utilisent souvent des chaînes d'une manière qui nécessite qu'elles soient immuables (il ne s'agit peut-être pas d'une chaîne , mais elle est immuable).

Conclusion

Ces considérations de conception ont été faites encore et encore dans une multitude de langues. C'est le consensus général que les chaînes immuables, malgré toute leur maladresse, sont meilleures que les alternatives et conduisent à un meilleur code (moins de bogues) et des exécutables plus rapides dans l'ensemble.

105
user40980

Raisons dont je me souviens:

  1. La fonctionnalité de pool de chaînes sans rendre la chaîne immuable n'est pas possible du tout car dans le cas du pool de chaînes, un objet chaîne/littéral, par exemple "XYZ" sera référencé par de nombreuses variables de référence, donc si l'une d'entre elles change, la valeur d'autres sera automatiquement affectée.

  2. String a été largement utilisé comme paramètre pour de nombreuses classes Java par exemple pour ouvrir une connexion réseau, pour ouvrir une connexion à une base de données, ouvrir des fichiers. Si String n'est pas immuable, cela entraînerait une menace sérieuse pour la sécurité.

  3. L'immutabilité permet à String de mettre en cache son code de hachage.

  4. Rend le thread-safe.

21
NINCOMPOOP

1) Pool de cordes

Le concepteur Java sait que String va être le type de données le plus utilisé dans toutes sortes d'applications Java et c'est pourquoi ils voulaient optimiser dès le départ. L'une des étapes clés dans cette direction a été l'idée de stocker String littéraux dans le pool de chaînes. L'objectif était de réduire les objets String temporaires en les partageant et pour les partager, ils doivent provenir de la classe Immutable. Vous ne pouvez pas partager un objet mutable avec deux parties inconnues l'une de l'autre. Prenons un exemple hypothétique, où deux variables de référence pointent vers le même objet String:

String s1 = "Java";
String s2 = "Java";

Maintenant, si s1 change l'objet de "Java" en "C++", la variable de référence a également la valeur s2 = "C++", qu'elle ne connaît même pas. En rendant String immuable, ce partage de String littéral était possible. En bref, l'idée clé du pool de chaînes ne peut pas être implémentée sans rendre la chaîne finale ou immuable en Java.

2) Sécurité

Java a un objectif clair en termes de fourniture d'un environnement sécurisé à tous les niveaux de service et String est essentiel dans tous ces aspects de sécurité. La chaîne a été largement utilisée comme paramètre pour de nombreuses classes Java, par exemple pour ouvrir une connexion réseau, vous pouvez passer l'hôte et le port en tant que chaîne, pour lire les fichiers en Java vous pouvez transmettre le chemin des fichiers et du répertoire en tant que chaîne et pour ouvrir la connexion à la base de données, vous pouvez transmettre l'URL de la base de données en tant que chaîne. Si la chaîne n'est pas immuable, un utilisateur peut avoir accordé l'accès à un fichier particulier dans le système, mais après l'authentification, il peut modifier le CHEMIN vers quelque chose d'autre, cela pourrait entraîner de graves problèmes de sécurité. De même, lors de la connexion à une base de données ou à toute autre machine du réseau, la mutation de la valeur de chaîne peut poser des menaces de sécurité. Les chaînes mutables peuvent également provoquer des problèmes de sécurité dans Reflection, car les paramètres sont des chaînes. .

3) Utilisation de la chaîne dans le mécanisme de chargement de classe

Une autre raison pour laquelle String final ou Immutable était motivée par le fait qu'elle était fortement utilisée dans le mécanisme de chargement des classes. Comme String n'a pas été immuable, un attaquant peut profiter de ce fait et demander de charger les classes standard Java par exemple Java.io.Reader peut être changé en classe malveillante com.unknown.DataStolenReader. Par en gardant la chaîne finale et immuable, nous pouvons au moins être sûrs que JVM charge les classes correctes.

4) Avantages du multithreading

Étant donné que la concurrence et le multithread étaient l'offre clé de Java, il était très logique de penser à la sécurité des threads des objets String. Puisqu'on s'attendait à ce que String soit largement utilisé, ce qui en fait Immutable signifie aucune synchronisation externe, signifie un code beaucoup plus propre impliquant le partage de String entre plusieurs threads. Cette fonctionnalité unique rend le codage concurrentiel déjà compliqué, déroutant et sujet aux erreurs beaucoup plus facile. Parce que String est immuable et que nous le partageons simplement entre les threads, il en résulte un code plus lisible.

5) Optimisation et performances

Maintenant, lorsque vous rendez une classe immuable, vous savez à l'avance que cette classe ne changera pas une fois créée. Cela garantit un chemin ouvert pour de nombreuses optimisations de performances, par exemple mise en cache. String lui-même le sait, je ne vais pas changer, donc String cache son hashcode. Il calcule même le hashcode paresseusement et une fois créé, il suffit de le mettre en cache. Dans un monde simple, lorsque vous appelez pour la première fois la méthode hashCode () d'un objet String, il calcule le code de hachage et tout appel ultérieur à hashCode () renvoie une valeur en cache déjà calculée. Cela se traduit par un bon gain de performances, étant donné que la chaîne est fortement utilisée dans les cartes basées sur le hachage, par exemple Hashtable et HashMap. La mise en cache du hashcode n'était pas possible sans le rendre immuable et définitif, car cela dépend du contenu de String lui-même.

7
saidesh kilaru

La machine virtuelle Java Java effectue plusieurs optimisations concernant les opérations de chaîne qui ne pourraient pas être effectuées autrement. Par exemple, si vous aviez une chaîne de valeur "Mississippi" et que vous avez affecté "Mississippi" .substring (0 , 4) à une autre chaîne, pour autant que vous le sachiez, une copie a été faite des quatre premiers caractères pour faire "Miss". Ce que vous ne savez pas, c'est que les deux partagent la même chaîne d'origine "Mississippi" avec l'un étant le propriétaire et l'autre étant une référence de cette chaîne de la position 0 à 4. (La référence au propriétaire empêche le propriétaire d'être collecté par le garbage collector lorsque le propriétaire sort du domaine)

C'est trivial pour une chaîne aussi petite que "Mississippi", mais avec des chaînes plus grandes et des opérations multiples, ne pas avoir à copier la chaîne est un gros gain de temps! Si les chaînes étaient mutables, vous ne pourriez pas le faire, car la modification de l'original affecterait également les "copies" de la sous-chaîne.

De plus, comme Donal le mentionne, l'avantage serait considérablement alourdi par son inconvénient. Imaginez que vous écrivez un programme qui dépend d'une bibliothèque et que vous utilisez une fonction qui renvoie une chaîne. Comment pouvez-vous être sûr que cette valeur restera constante? Pour éviter que cela ne se produise, vous devez toujours en produire une copie.

Et si vous avez deux threads partageant la même chaîne? Vous ne voudriez pas lire une chaîne en cours de réécriture par un autre thread, n'est-ce pas? La chaîne devrait donc être thread-safe, ce qui étant la classe courante, elle rendrait pratiquement tous les programmes Java beaucoup plus lents. Sinon, vous devriez faire une copie pour chaque thread qui nécessite cette chaîne ou vous devrez mettre le code en utilisant cette chaîne dans un bloc de synchronisation, qui ne font que ralentir votre programme.

Pour toutes ces raisons, c'était l'une des premières décisions prises pour Java afin de se différencier de C++.

5
Neil

La raison de l'immuabilité de la chaîne vient de la cohérence avec les autres types primitifs du langage. Si vous avez un int contenant la valeur 42 et que vous y ajoutez la valeur 1, vous ne modifiez pas le 42. Vous obtenez une nouvelle valeur, 43, qui n'a aucun lien avec les valeurs de départ. La mutation de primitives autres que chaîne n'a aucun sens conceptuel; et en tant que tels programmes qui traitent les chaînes comme immuables sont souvent plus faciles à raisonner et à comprendre.

De plus, Java fournit vraiment à la fois des chaînes mutables et immuables, comme vous le voyez avec StringBuilder; vraiment, seule la valeur par défaut est la chaîne immuable. Si vous souhaitez transmettre des références à StringBuilder partout, vous êtes parfaitement invités à le faire. Java utilise des types distincts (String et StringBuilder) pour ces concepts, car il ne prend pas en charge l'expression de la mutabilité ou son absence dans son système de types. Dans les langages qui prennent en charge l'immuabilité dans leurs systèmes de types (par exemple, C++ const), il existe souvent un seul type de chaîne qui remplit les deux fonctions.

Oui, avoir une chaîne immuable permet d'implémenter certaines optimisations spécifiques aux chaînes immuables, telles que l'internement, et permet de transmettre des références de chaîne sans synchronisation entre les threads. Cependant, cela confond le mécanisme avec l'objectif visé d'une langue avec un système de type simple et cohérent. Je compare cela à la façon dont tout le monde pense à la collecte des ordures dans le mauvais sens; la récupération de place n'est pas une "récupération de mémoire inutilisée"; c'est une "simulation d'un ordinateur avec une mémoire illimitée" . Les optimisations de performances discutées sont des choses qui sont faites pour que l'objectif de chaînes immuables fonctionne bien sur de vraies machines; pas la raison pour laquelle de telles chaînes sont immuables en premier lieu.

5
Billy ONeal

L'immuabilité signifie que les constantes détenues par des classes que vous ne possédez pas ne peuvent pas être modifiées. Les classes que vous ne possédez pas incluent celles qui sont au cœur de l'implémentation de Java, et les chaînes qui ne doivent pas être modifiées incluent des éléments comme les jetons de sécurité, les adresses de service, etc. Vous vraiment ne devriez pas ' t être en mesure de modifier ce genre de choses (et cela s'applique doublement lors du fonctionnement en mode bac à sable).

Si String n'était pas immuable, chaque fois que vous le récupériez dans un contexte qui ne voulait pas que le contenu de la chaîne change sous ses pieds, vous devriez en prendre une copie "au cas où". Cela coûte très cher.

4
Donal Fellows

Imaginez un système où vous acceptez certaines données, vérifiez leur exactitude, puis les transmettez (pour être stockées dans une base de données, par exemple).

En supposant que les données sont un String et qu'elles doivent contenir au moins 5 caractères. Votre méthode ressemble à ceci:

public void handle(String input) {
  if (input.length() < 5) {
    throw new IllegalArgumentException();
  }
  storeInDatabase(input);
}

Nous pouvons maintenant convenir que lorsque storeInDatabase est appelé ici, le input répondra à l'exigence. Mais si String était modifiable, alors l'appelant pourrait modifier l'objet input ( à partir d'un autre thread) juste après avoir été vérifié et avant qu'il ne soit stocké dans la base de données . Cela nécessiterait un bon timing et probablement n'irait pas bien à chaque fois, mais de temps en temps, il serait en mesure de vous faire stocker des valeurs non valides dans la base de données .

Les types de données immuables sont une solution très simple à ce problème (et à beaucoup de problèmes connexes): chaque fois que vous vérifiez une valeur, vous pouvez dépendre du fait que la condition vérifiée est toujours vraie plus tard.

2
Joachim Sauer

En général, vous rencontrerez types de valeur et types de référence. Avec un type de valeur, vous ne vous souciez pas de l'objet qui le représente, vous vous souciez de la valeur. Si je vous donne une valeur, vous vous attendez à ce que cette valeur reste la même. Vous ne voulez pas que cela change soudainement. Le nombre 5 est une valeur. Vous ne vous attendez pas à ce qu'il passe à 6 soudainement. La chaîne "Bonjour" est une valeur. Vous ne vous attendez pas à ce qu'il passe soudainement à "P *** off".

Avec types de référence vous vous souciez de l'objet et vous vous attendez à ce qu'il change. Par exemple, vous vous attendez souvent à ce qu'un tableau change. Si je vous donne un tableau et que vous souhaitez le conserver tel quel, vous devez soit me faire confiance pour ne pas le modifier, soit vous en faire une copie.

Avec la classe de chaînes Java, les concepteurs devaient prendre une décision: est-il préférable que les chaînes se comportent comme un type de valeur, ou devraient-elles se comporter comme un type de référence? Dans le cas de Java strings, la décision a été prise qu'ils devraient être des types de valeur, ce qui signifie qu'étant des objets, ils doivent être des objets immuables.

La décision contraire aurait pu être prise, mais à mon avis, cela aurait causé beaucoup de maux de tête. Comme indiqué ailleurs, de nombreuses langues ont pris la même décision et sont arrivées à la même conclusion. Une exception est C++, qui a une classe de chaîne, et les chaînes peuvent être constantes ou non constantes, mais en C++, contrairement à Java, les paramètres d'objet peuvent être passés en tant que valeurs et non en tant que références.

0
gnasher729

Je suis vraiment surpris que personne ne l’ait signalé.

Réponse: Cela ne vous serait pas très bénéfique, même s'il était modifiable. Cela ne vous profiterait pas autant que cela vous causerait des problèmes supplémentaires. Examinons deux cas de mutation les plus courants:

Changer un caractère d'une chaîne

Puisque chaque caractère dans une chaîne Java prend 2 ou 4 octets, demandez-vous, gagneriez-vous quelque chose si vous pouviez muter la copie existante?

Dans le scénario où vous remplacez un caractère de 2 octets par un 4 octets (ou vice-versa), vous devez décaler la partie restante de la chaîne de 2 octets vers la gauche ou vers la droite. Ce qui n'est pas si différent de copier la chaîne entière du point de vue informatique.

Il s'agit également d'un comportement vraiment irrégulier qui est généralement indésirable. Imaginez quelqu'un tester une application avec du texte anglais, et lorsque l'application est adoptée dans des pays étrangers, comme la Chine, le tout commence à fonctionner étrangement.

Ajout d'une autre chaîne (ou caractère) à la chaîne existante

Si vous avez deux chaînes arbitraires, celles-ci se trouvent à deux emplacements de mémoire distincts. Si vous souhaitez modifier la première en ajoutant la seconde, vous ne pouvez pas simplement demander de la mémoire supplémentaire à la fin de la première chaîne, car elle est probablement déjà occupée.

Vous devez copier la chaîne concaténée vers un tout nouvel emplacement, qui est exactement le même que si les deux chaînes étaient immuables.

Si vous souhaitez effectuer des ajouts de manière efficace, vous pouvez utiliser StringBuilder, qui réserve une assez grande quantité d'espace à la fin d'une chaîne, juste à cette fin pour un éventuel futur ajout.

0
Rok Kralj