Pourquoi ont-ils décidé de rendre la chaîne immuable dans Java et .NET (et d'autres langages)? Pourquoi ne l'ont-ils pas rendu mutable?
Selon Effective Java , chapitre 4, page 73, 2e édition:
"Il y a plusieurs bonnes raisons à cela: les classes immuables sont plus faciles à concevoir, implémenter et utiliser que les classes mutables. Elles sont moins sujettes aux erreurs et sont plus sécurisées.
[...]
" Les objets immuables sont simples. Un objet immuable peut être dans un seul état, l'état dans lequel il a été créé. Si vous vous assurez que tous les constructeurs établissent invariants de classe, alors il est garanti que ces invariants resteront vrais pour toujours, sans effort de votre part.
[...]
Les objets immuables sont intrinsèquement thread-safe; ils ne nécessitent aucune synchronisation. Ils ne peuvent pas être corrompus par plusieurs threads y accédant simultanément. C'est de loin l'approche la plus simple pour garantir la sécurité des filetages. En fait, aucun thread ne peut jamais observer l'effet d'un autre thread sur un objet immuable. Par conséquent, les objets immuables peuvent être partagés librement
[...]
Autres petits points du même chapitre:
Non seulement vous pouvez partager des objets immuables, mais vous pouvez également partager leurs éléments internes.
[...]
Les objets immuables font de grands blocs de construction pour d'autres objets, qu'ils soient mutables ou immuables.
[...]
Le seul inconvénient réel des classes immuables est qu'elles nécessitent un objet séparé pour chaque valeur distincte.
Il y a au moins deux raisons.
Premièrement - sécurité http://www.javafaq.nu/Java-article1060.html
La principale raison pour laquelle String a rendu immuable était la sécurité. Regardez cet exemple: nous avons une méthode d'ouverture de fichier avec vérification de connexion. Nous passons une chaîne à cette méthode pour traiter l'authentification qui est nécessaire avant que l'appel ne soit transmis à OS. Si String était mutable, il était possible de modifier son contenu après la vérification d'authentification avant que le système d'exploitation ne reçoive la demande du programme, il est alors possible de demander n'importe quel fichier. Donc, si vous avez le droit d'ouvrir le fichier texte dans le répertoire utilisateur, mais à la volée lorsque vous réussissez à changer le nom du fichier, vous pouvez demander à ouvrir le fichier "passwd" ou tout autre. Ensuite, un fichier peut être modifié et il sera possible de se connecter directement au système d'exploitation.
Deuxième - Efficacité de la mémoire http://hikrish.blogspot.com/2006/07/why-string-class-is-immutable. html
JVM gère en interne le "String Pool". Pour atteindre l'efficacité de la mémoire, la JVM fera référence à l'objet String du pool. Il ne créera pas les nouveaux objets String. Ainsi, chaque fois que vous créez un nouveau littéral de chaîne, JVM vérifie dans le pool s'il existe déjà ou non. S'il est déjà présent dans le pool, donnez simplement la référence au même objet ou créez le nouvel objet dans le pool. Il y aura de nombreuses références pointant vers les mêmes objets String, si quelqu'un modifie la valeur, cela affectera toutes les références. Donc, Sun a décidé de le rendre immuable.
En fait, la chaîne des raisons est immuable dans Java n'a pas grand-chose à voir avec la sécurité. Les deux raisons principales sont les suivantes:
Les chaînes sont un type d'objet extrêmement largement utilisé. Il est donc plus ou moins garanti d'être utilisé dans un environnement multi-thread. Les chaînes sont immuables pour vous assurer qu'il est sûr de partager des chaînes entre les threads. Avoir une chaîne immuable garantit que lors du passage de chaînes du thread A à un autre thread B, le thread B ne peut pas modifier de façon inattendue la chaîne du thread A.
Non seulement cela aide à simplifier la tâche déjà assez compliquée de la programmation multithread, mais cela contribue également aux performances des applications multithread. L'accès aux objets modifiables doit en quelque sorte être synchronisé lorsqu'ils sont accessibles à partir de plusieurs threads, pour vous assurer qu'un thread n'essaie pas de lire la valeur de votre objet pendant qu'il est modifié par un autre thread. Une bonne synchronisation est à la fois difficile à faire correctement pour le programmeur et coûteuse à l'exécution. Les objets immuables ne peuvent pas être modifiés et n'ont donc pas besoin de synchronisation.
Bien que l'internalisation de chaînes ait été mentionnée, elle ne représente qu'un petit gain d'efficacité en mémoire pour les programmes Java. Seuls les littéraux de chaînes sont internés. Cela signifie que seules les chaînes qui sont les mêmes dans votre code source partagera le même objet String. Si votre programme crée dynamiquement des chaînes identiques, elles seront représentées dans des objets différents.
Plus important encore, des chaînes immuables leur permettent de partager leurs données internes. Pour de nombreuses opérations de chaîne, cela signifie que le tableau de caractères sous-jacent n'a pas besoin d'être copié. Par exemple, supposons que vous souhaitiez prendre les cinq premiers caractères de String. En Java, vous appelleriez myString.substring (0,5). Dans ce cas, la méthode substring () consiste simplement à créer un nouvel objet String qui partage le char sous-jacent de myString [] mais qui sait qu'il commence à l'index 0 et se termine à l'index 5 de ce char []. Pour mettre cela sous forme graphique, vous vous retrouveriez avec ce qui suit:
| myString |
v v
"The quick brown fox jumps over the lazy dog" <-- shared char[]
^ ^
| | myString.substring(0,5)
Cela rend ce type d'opérations extrêmement bon marché et O(1) puisque l'opération ne dépend ni de la longueur de la chaîne d'origine, ni de la longueur de la sous-chaîne que nous devons extraire. Ce comportement a également quelques avantages de mémoire, car de nombreuses chaînes peuvent partager leur caractère sous-jacent [].
Sécurité et performance des fils. Si une chaîne ne peut pas être modifiée, il est sûr et rapide de passer une référence parmi plusieurs threads. Si les chaînes étaient modifiables, vous devrez toujours copier tous les octets de la chaîne dans une nouvelle instance ou assurer la synchronisation. Une application typique lira une chaîne 100 fois pour chaque fois que cette chaîne doit être modifiée. Voir wikipedia sur immuabilité .
On devrait vraiment se demander, "pourquoi X devrait-il être mutable?" Il est préférable de choisir l'immuabilité par défaut, en raison des avantages déjà mentionnés par Princess Fluff . Ce devrait être une exception que quelque chose est mutable.
Malheureusement, la plupart des langages de programmation actuels utilisent par défaut la mutabilité, mais avec un peu de chance à l'avenir, la valeur par défaut est davantage sur l'immutabilité (voir ne liste de souhaits pour le prochain langage de programmation principal ).
La chaîne n'est pas un type primitif, mais vous voulez normalement l'utiliser avec la sémantique des valeurs, c'est-à-dire comme une valeur.
Une valeur est quelque chose en laquelle vous pouvez avoir confiance ne changera pas derrière votre dos. Si vous écrivez: String str = someExpr();
Vous ne voulez pas que cela change à moins que VOUS ne fassiez quelque chose avec str.
La chaîne en tant qu'objet a naturellement une sémantique de pointeur, pour obtenir également une sémantique de valeur, elle doit être immuable.
Hou la la! Je ne peux pas croire la désinformation ici. Les cordes immuables n'ont rien de sûr. Si quelqu'un a déjà accès aux objets dans une application en cours d'exécution (ce qui devrait être supposé si vous essayez de vous prémunir contre quelqu'un `` piratant '' une chaîne dans votre application), il y aurait certainement beaucoup d'autres opportunités disponibles pour le piratage.
C'est une idée assez nouvelle que l'immuabilité de String résout les problèmes de threading. Hmmm ... J'ai un objet qui est modifié par deux threads différents. Comment résoudre ça? synchroniser l'accès à l'objet? Naawww ... ne laissons personne changer d'objet - cela résoudra tous nos problèmes de concurrence désordonnés! En fait, rendons tous les objets immuables, puis nous pouvons supprimer le produit synchronisé du langage Java.
La vraie raison (soulignée par d'autres ci-dessus) est l'optimisation de la mémoire. Il est assez courant dans toute application que le même littéral de chaîne soit utilisé à plusieurs reprises. Il est si courant, en fait, qu'il y a des décennies, de nombreux compilateurs ont optimisé le stockage d'une seule instance d'un littéral de chaîne. L'inconvénient de cette optimisation est que le code d'exécution qui modifie un littéral de chaîne introduit un problème car il modifie l'instance de tout autre code qui le partage. Par exemple, il ne serait pas bon pour une fonction quelque part dans une application de changer la chaîne littérale "dog" en "cat". Un printf ("chien") entraînerait l'écriture de "chat" sur stdout. Pour cette raison, il devait y avoir un moyen de se prémunir contre le code qui tente de changer les littéraux de chaîne (c'est-à-dire les rendre immuables). Certains compilateurs (avec le soutien du système d'exploitation) accompliraient cela en plaçant le littéral de chaîne dans un segment de mémoire en lecture seule spécial qui provoquerait une erreur de mémoire si une tentative d'écriture était effectuée.
Dans Java c'est ce qu'on appelle l'internement. Le compilateur Java ici ne fait que suivre une optimisation de mémoire standard effectuée par les compilateurs pendant des décennies. Et pour résoudre le même problème de ces littéraux de chaîne étant modifiés au moment de l'exécution, Java rend simplement la classe String immuable (c'est-à-dire, ne vous donne aucun paramètre qui vous permettrait de modifier le contenu de String). Les chaînes n'auraient pas à être immuable si l'internement des littéraux de chaîne n'a pas eu lieu.
Un facteur est que, si les chaînes étaient mutables, les objets stockant des chaînes devraient faire attention à stocker des copies, de peur que leurs données internes ne changent sans préavis. Étant donné que les chaînes sont un type assez primitif comme les nombres, c'est bien quand on peut les traiter comme si elles étaient passées par valeur, même si elles sont passées par référence (ce qui aide également à économiser de la mémoire).
Je sais que c'est une bosse, mais ... Sont-ils vraiment immuables? Considérer ce qui suit.
public static unsafe void MutableReplaceIndex(string s, char c, int i)
{
fixed (char* ptr = s)
{
*((char*)(ptr + i)) = c;
}
}
...
string s = "abc";
MutableReplaceIndex(s, '1', 0);
MutableReplaceIndex(s, '2', 1);
MutableReplaceIndex(s, '3', 2);
Console.WriteLine(s); // Prints 1 2 3
Vous pouvez même en faire une méthode d'extension.
public static class Extensions
{
public static unsafe void MutableReplaceIndex(this string s, char c, int i)
{
fixed (char* ptr = s)
{
*((char*)(ptr + i)) = c;
}
}
}
Ce qui fait le travail suivant
s.MutableReplaceIndex('1', 0);
s.MutableReplaceIndex('2', 1);
s.MutableReplaceIndex('3', 2);
Conclusion: Ils sont dans un état immuable connu du compilateur. Bien sûr, ce qui précède s'applique uniquement aux chaînes .NET car Java n'a pas de pointeurs. Cependant, une chaîne peut être entièrement modifiable à l'aide de pointeurs en C #. Ce n'est pas la façon dont les pointeurs sont destinés à être utilisés, a utilisation pratique ou est utilisé en toute sécurité; il est cependant possible, ce qui permet de plier l'ensemble de la règle "mutable". Vous ne pouvez normalement pas modifier un index directement d'une chaîne et c'est le seul moyen. Il existe un moyen d'éviter cela en interdisant le pointeur instances de chaînes ou faire une copie lorsqu'une chaîne est pointée, mais aucune n'est effectuée, ce qui rend les chaînes en C # pas entièrement immuables.
Dans la plupart des cas, une "chaîne" est (utilisée/traitée comme/considérée/supposée être) une unité atomique, significative, tout comme un nombre .
Tu devrais savoir pourquoi. Pensez-y.
Je déteste le dire, mais malheureusement nous en débattons parce que notre langue craint, et nous essayons d'utiliser un seul mot, chaîne , pour décrire un concept ou une classe d'objet complexe et contextuellement situé.
Nous effectuons des calculs et des comparaisons avec des "chaînes" similaires à la façon dont nous procédons avec les nombres. Si les chaînes (ou entiers) étaient mutables, nous aurions à écrire du code spécial pour verrouiller leurs valeurs dans des formes locales immuables afin d'effectuer tout type de calcul de manière fiable. Par conséquent, il est préférable de penser à une chaîne comme un identificateur numérique, mais au lieu d'avoir une longueur de 16, 32 ou 64 bits, elle pourrait être longue de plusieurs centaines de bits.
Quand quelqu'un dit "chaîne", nous pensons tous à des choses différentes. Ceux qui le considèrent simplement comme un ensemble de personnages, sans but particulier à l'esprit, seront bien sûr consternés que quelqu'un vient de décider qu'ils ne devraient pas être capable de manipuler ces personnages. Mais la classe "string" n'est pas seulement un tableau de caractères. C'est un STRING
, pas un char[]
. Il existe quelques hypothèses de base sur le concept que nous appelons une "chaîne", et il peut généralement être décrit comme une unité atomique significative de données codées comme un nombre. Lorsque les gens parlent de "manipuler des chaînes", ils parlent peut-être vraiment de manipuler caractères pour construire chaînes, et un StringBuilder est parfait pour cela. Réfléchissez un peu à ce que le mot "chaîne" signifie vraiment.
Considérez un instant ce que ce serait si les cordes étaient mutables. La fonction API suivante peut être amenée à renvoyer des informations pour un autre utilisateur si la chaîne de nom d'utilisateur mutable est modifiée intentionnellement ou non par un autre thread pendant que cette fonction utilise il:
string GetPersonalInfo( string username, string password )
{
string stored_password = DBQuery.GetPasswordFor( username );
if (password == stored_password)
{
//another thread modifies the mutable 'username' string
return DBQuery.GetPersonalInfoFor( username );
}
}
La sécurité ne concerne pas seulement le "contrôle d'accès", elle concerne également la "sécurité" et la "garantie d'exactitude". Si une méthode ne peut pas être facilement écrite et dépendante pour effectuer un calcul ou une comparaison simple de manière fiable, alors ce n'est pas sûr de l'appeler, mais il serait sûr de remettre en question le langage de programmation lui-même.
L'immuabilité n'est pas si étroitement liée à la sécurité. Pour cela, au moins dans .NET, vous obtenez la classe SecureString.
Les chaînes en Java ne sont pas vraiment immuables, vous pouvez modifier leur valeur en utilisant la réflexion et/ou le chargement de classe. Vous ne devez pas dépendre de cette propriété pour la sécurité. Pour des exemples, voir: Magic Trick En Java
C'est un compromis. Les chaînes entrent dans le pool de chaînes et lorsque vous créez plusieurs chaînes identiques, elles partagent la même mémoire. Les concepteurs ont pensé que cette technique d'économie de mémoire fonctionnerait bien dans le cas commun, car les programmes ont tendance à beaucoup broyer les mêmes chaînes.
L'inconvénient est que les concaténations font beaucoup de chaînes supplémentaires qui ne sont que transitoires et deviennent simplement des ordures, nuisant en fait aux performances de la mémoire. Vous avez StringBuffer et StringBuilder (en Java, StringBuilder est également en .NET) à utiliser pour préserver la mémoire dans ces cas.
La décision d'avoir une chaîne mutable en C++ pose beaucoup de problèmes, voir cet excellent article de Kelvin Henney sur Mad COW Disease .
COW = Copy On Write.
L'immuabilité est bonne. Voir Java efficace. Si vous deviez copier une chaîne à chaque fois que vous la passiez, ce serait beaucoup de code sujet aux erreurs. Vous avez également une confusion quant aux modifications qui affectent quelles références. De la même manière que Integer doit être immuable pour se comporter comme int, les chaînes doivent se comporter comme immuables pour agir comme des primitives. En C++, passer des chaînes par valeur le fait sans mention explicite dans le code source.
Il existe une exception pour presque toutes les règles:
using System;
using System.Runtime.InteropServices;
namespace Guess
{
class Program
{
static void Main(string[] args)
{
const string str = "ABC";
Console.WriteLine(str);
Console.WriteLine(str.GetHashCode());
var handle = GCHandle.Alloc(str, GCHandleType.Pinned);
try
{
Marshal.WriteInt16(handle.AddrOfPinnedObject(), 4, 'Z');
Console.WriteLine(str);
Console.WriteLine(str.GetHashCode());
}
finally
{
handle.Free();
}
}
}
}