web-dev-qa-db-fra.com

Une chaîne Java est-elle vraiment immuable?

Nous savons tous que String est immuable en Java, mais vérifiez le code suivant:

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  

Pourquoi ce programme fonctionne-t-il comme ça? Et pourquoi la valeur de s1 et s2 est-elle modifiée, mais pas s3?

381
Darshan Patel

String est immuable * mais cela signifie seulement que vous ne pouvez pas le changer en utilisant son API publique. 

Ce que vous faites ici, c'est contourner l'API normale, en utilisant la réflexion. De la même manière, vous pouvez modifier les valeurs des énumérations, la table de consultation utilisée dans la liste déroulante automatique Integer, etc.

Maintenant, la raison pour laquelle s1 et s2 changent de valeur, c'est qu'ils font tous les deux référence à la même chaîne internée. Le compilateur fait cela (comme mentionné par d'autres réponses). 

La raison pour laquelle s3 fait not était en fait un peu surprenant pour moi, car je pensais qu'il partagerait le tableau value ( c'était le cas dans les versions précédentes de Java , avant Java 7u6). Cependant, en regardant le code source de String, nous pouvons voir que le tableau de caractères value pour une sous-chaîne est réellement copié (en utilisant Arrays.copyOfRange(..)). C'est pourquoi cela reste inchangé.

Vous pouvez installer une SecurityManager, pour éviter que du code malveillant ne fasse de telles choses. Mais gardez à l'esprit que certaines bibliothèques dépendent de ce type d'astuces de réflexion (généralement des outils ORM, des bibliothèques AOP, etc.).

*) Au départ, j’écrivais que Strings ne sont pas vraiment immuables, mais "efficaces immuables". Cela peut être trompeur dans l'implémentation actuelle de String, où le tableau value est bien marqué private final. Il convient toutefois de noter qu'il n'y a aucun moyen de déclarer un tableau en Java comme immuable, vous devez donc veiller à ne pas l'exposer en dehors de sa classe, même avec les modificateurs d'accès appropriés.


Comme ce sujet semble extrêmement populaire, voici quelques suggestions de lectures supplémentaires: Discussion Reflection Madness de Heinz Kabutz de JavaZone 2009, qui couvre de nombreux problèmes du PO, ainsi que d’autres réflexions ... eh bien ... folie. 

Cela explique pourquoi cela est parfois utile. Et pourquoi, la plupart du temps, vous devriez l’éviter. :-)

394
haraldK

En Java, si deux variables primitives de chaîne sont initialisées avec le même littéral, la même référence est affectée aux deux variables:

String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true

initialization

C’est la raison pour laquelle la comparaison est vraie. La troisième chaîne est créée à l'aide de substring() qui crée une nouvelle chaîne au lieu de pointer de la même manière.

sub string

Lorsque vous accédez à une chaîne en utilisant la réflexion, vous obtenez le pointeur réel:

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

Donc, changer ceci changera la chaîne contenant un pointeur, mais comme s3 est créé avec une nouvelle chaîne en raison de substring(), cela ne changera pas.

change

94
Zaheer Ahmed

Vous utilisez la réflexion pour contourner l'immuabilité de String - c'est une forme "d'attaque".

Vous pouvez créer de nombreux exemples comme celui-ci (par exemple, vous pouvez même instancier un objet Void aussi), mais cela ne signifie pas que String n'est pas "immuable".

Il existe des cas d'utilisation où ce type de code peut être utilisé à votre avantage et constituer un "bon code", tel que effacement des mots de passe de la mémoire le plus tôt possible (avant la GC) .

Selon le responsable de la sécurité, vous ne pourrez peut-être pas exécuter votre code.

50
Bohemian

Vous utilisez la réflexion pour accéder aux "détails d'implémentation" de l'objet string. L’immuabilité est la caractéristique de l’interface publique d’un objet.

30
Ankur

Les modificateurs de visibilité et final (c'est-à-dire l'immuabilité) ne constituent pas une mesure par rapport au code malveillant en Java; ce ne sont que des outils pour se protéger contre les erreurs et rendre le code plus facile à gérer (un des principaux arguments de vente du système). C'est pourquoi vous pouvez accéder aux détails de l'implémentation interne, tels que le tableau de caractères de sauvegarde pour Strings, par réflexion.

Le deuxième effet que vous voyez est que toutes les Strings changent alors qu'il semble que vous ne changiez que s1. C’est une propriété des littéraux Java String qu’ils sont automatiquement internés, c’est-à-dire mis en cache. Deux littéraux de chaîne avec la même valeur seront en fait le même objet. Lorsque vous créez une chaîne avec new, elle ne sera pas internée automatiquement et vous ne verrez pas cet effet.

#substring jusqu'à récemment (Java 7u6) fonctionnait de la même manière, ce qui aurait expliqué le comportement de la version d'origine de votre question. Il n'a pas créé de nouveau tableau de caractères de sauvegarde, mais a réutilisé celui de la chaîne d'origine. il vient de créer un nouvel objet String qui utilise un décalage et une longueur pour présenter uniquement une partie de ce tableau. Cela a généralement fonctionné car les cordes sont immuables - à moins que vous ne le contourniez. Cette propriété de #substring signifiait également que la totalité de la chaîne d'origine ne pouvait pas être nettoyée alors qu'une sous-chaîne plus courte créée à partir de celle-ci existait toujours.

En ce qui concerne Java actuel et votre version actuelle de la question, il n’ya pas de comportement étrange de #substring.

24

L'immuabilité des chaînes est du point de vue de l'interface. Vous utilisez la réflexion pour contourner l'interface et modifier directement les éléments internes des instances String.

s1 et s2 sont tous deux modifiés car ils sont tous deux affectés à la même instance de chaîne "intern". Vous pouvez en apprendre un peu plus sur cette partie dans cet article sur l'égalité des chaînes et l'interning. Vous serez peut-être surpris de découvrir que, dans votre exemple de code, s1 == s2 renvoie true!

11
Krease

Quelle version de Java utilisez-vous? Depuis Java 1.7.0_06, Oracle a modifié la représentation interne de String, en particulier la sous-chaîne. 

Citating from Représentation interne des chaînes dans Oracle Tunes Java :

Dans le nouveau paradigme, les champs Décalage et Nombre de chaînes ont été supprimés, de sorte que les sous-chaînes ne partagent plus la valeur char [] sous-jacente. 

Avec ce changement, cela peut arriver sans réflexion (???).

10
manikanta

Il y a vraiment deux questions ici:

  1. Les chaînes sont-elles vraiment immuables?
  2. Pourquoi s3 n'est-il pas modifié?

Point 1: À l'exception de ROM, votre ordinateur ne possède pas de mémoire immuable. De nos jours, même ROM est parfois accessible en écriture. Il y a toujours du code quelque part (que ce soit le noyau ou le code natif contournant votre environnement géré) pouvant écrire sur votre adresse mémoire. Donc, dans la "réalité", non, ils ne sont pas absolument immuables.

Point 2: Cela est dû au fait que la sous-chaîne alloue probablement une nouvelle instance de chaîne, qui copie probablement le tableau. Il est possible d'implémenter la sous-chaîne de telle sorte qu'elle ne fasse pas de copie, mais cela ne veut pas dire que c'est le cas. Il y a des compromis impliqués.

Par exemple, si vous tenez une référence à reallyLargeString.substring(reallyLargeString.length - 2), une grande quantité de mémoire sera-t-elle conservée en vie, ou seulement quelques octets?

Cela dépend de la manière dont la sous-chaîne est implémentée. Une copie en profondeur conservera moins de mémoire vive, mais elle fonctionnera un peu plus lentement. Une copie superficielle gardera plus de mémoire vive, mais ce sera plus rapide. L'utilisation d'une copie en profondeur peut également réduire la fragmentation du tas, étant donné que l'objet chaîne et son tampon peuvent être alloués dans un bloc, par opposition à deux allocations de tas distinctes.

Quoi qu'il en soit, il semble que votre machine virtuelle Java ait choisi d'utiliser des copies complètes pour les appels de sous-chaîne.

7
Scott Wisniewski

Pour ajouter à la réponse de @ haraldK, il s'agit d'un hack de sécurité qui pourrait avoir un impact sérieux sur l'application. 

La première chose à faire est de modifier une chaîne constante stockée dans un pool de chaînes. Lorsque la chaîne est déclarée en tant que String s = "Hello World";, elle est placée dans un pool d'objets spécial en vue d'une réutilisation potentielle ultérieure. Le problème est que le compilateur placera une référence à la version modifiée au moment de la compilation et qu'une fois que l'utilisateur aura modifié la chaîne stockée dans ce pool lors de l'exécution, toutes les références dans le code pointeront vers la version modifiée. Cela donnerait lieu à un bogue suivant:

System.out.println("Hello World"); 

Imprimera:

Hello Java!

Il y avait un autre problème que j'ai rencontré lorsque je mettais en œuvre un calcul lourd sur des chaînes aussi risquées. Il y avait un bug qui est arrivé dans environ 1 fois sur 1 000 fois lors du calcul qui rendait le résultat indéterministe. J'ai pu trouver le problème en éteignant le JIT. J'obtenais toujours le même résultat avec le JIT désactivé. Je suppose que la raison en est que ce hack de sécurité String a cassé certains des contrats d’optimisation de JIT.

5
Andrey Chaschev

Selon le concept de pooling, toutes les variables String contenant la même valeur pointeront vers la même adresse mémoire. Par conséquent, s1 et s2, tous deux contenant la même valeur de «Hello World», pointeront vers le même emplacement mémoire (disons M1).

Par contre, s3 contient «World», il indiquera donc une allocation de mémoire différente (disons M2).

Alors maintenant, ce qui se passe, c'est que la valeur de S1 est modifiée (en utilisant la valeur char []). Ainsi, la valeur à l'emplacement de mémoire M1 désigné à la fois par s1 et s2 a été modifiée.

Par conséquent, l'emplacement de mémoire M1 a été modifié, ce qui entraîne une modification de la valeur de s1 et de s2.

Mais la valeur de l'emplacement M2 reste inchangée, donc s3 contient la même valeur d'origine.

5
AbhijeetMishra

La raison pour laquelle s3 ne change pas, c'est parce qu'en Java, lorsque vous créez une sous-chaîne, le tableau de caractères de valeur correspondant à une sous-chaîne est copié en interne (à l'aide de Arrays.copyOfRange ()).

s1 et s2 sont identiques car, en Java, ils se réfèrent tous deux à la même chaîne internée. C'est par conception en Java.

4

String est immuable, mais par réflexion, vous êtes autorisé à modifier la classe String. Vous venez de redéfinir la classe String comme étant mutable en temps réel. Vous pouvez redéfinir les méthodes pour qu'elles soient publiques, privées ou statiques si vous le souhaitez.

2
SpacePrez

[Clause de non-responsabilité, c’est un style de réponse délibérément fondé sur l’opinion, car j’estime que la réponse «ne faites pas ça à la maison, les enfants» est justifiée. 

Le péché est la ligne field.setAccessible(true); qui dit de violer l’API publique en permettant l’accès à un champ privé. C'est un trou de sécurité géant qui peut être verrouillé en configurant un gestionnaire de sécurité. 

Le phénomène dans la question est constitué de détails d’implémentation que vous ne verriez jamais si vous n’utilisez pas cette ligne de code dangereuse pour violer les modificateurs d’accès par réflexion. Clairement, deux chaînes (normalement) immuables peuvent partager le même tableau de caractères. Si une sous-chaîne partage le même tableau dépend de sa capacité et de la volonté du développeur de le partager. Normalement, ce sont des détails d'implémentation invisibles que vous ne devriez pas avoir à connaître à moins que vous ne tiriez le modificateur d'accès à travers la tête avec cette ligne de code. 

Ce n’est tout simplement pas une bonne idée de s’appuyer sur de tels détails qui ne peuvent pas être expérimentés sans violer les modificateurs d’accès en utilisant la réflexion. Le propriétaire de cette classe ne prend en charge que l'API publique normale et est libre d'apporter des modifications d'implémentation à l'avenir. 

Cela dit, la ligne de code est vraiment très utile quand une arme à feu vous tient la tête, vous obligeant à faire des choses aussi dangereuses. Utiliser cette porte arrière est généralement une odeur de code que vous devez mettre à niveau vers un meilleur code de bibliothèque, sans pécher. Une autre utilisation courante de cette ligne de code dangereuse consiste à écrire un "cadre vaudou" (orm, conteneur d'injection, ...). Beaucoup de gens deviennent religieux à propos de tels cadres (à la fois pour et contre eux), je vais donc éviter d'éviter une guerre des flammes en ne disant rien d'autre que la grande majorité des programmeurs n'ont pas à y aller. 

1
simbo1905

Les chaînes sont créées dans la zone permanente de la mémoire de la machine virtuelle Java. Donc oui, c'est vraiment immuable et ne peut pas être modifié après avoir été créé ... Parce que dans la JVM, il existe trois types de mémoire de tas: 1. Jeune génération 2. Ancienne génération 3. génération permanente.

Lorsqu'un objet est créé, il entre dans la zone de mémoire de la nouvelle génération et dans la zone PermGen réservées au pooling de chaînes.

Voici plus de détails sur lesquels vous pouvez aller chercher plus d’informations sur: Fonctionnement de Garbage Collection en Java.

String est de nature immuable, car il n’existe pas de méthode pour modifier l’objet String . C’est la raison pour laquelle ils ont introduit StringBuilder et StringBuffer classes 

0
Pratik Sherdiwala