Une chaîne Java est-elle vraiment immuable?

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

 Ssortingng s1 = "Hello World"; Ssortingng s2 = "Hello World"; Ssortingng s3 = s1.subssortingng(6); System.out.println(s1); // Hello World System.out.println(s2); // Hello World System.out.println(s3); // World Field field = Ssortingng.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 changé, mais pas s3 ?

Ssortingng est immuable * mais cela signifie uniquement que vous ne pouvez pas le modifier en utilisant son API publique.

Ce que vous faites ici est de contourner l’API normale, en utilisant la reflection. De la même manière, vous pouvez modifier les valeurs des énumérations, modifier la table de recherche utilisée dans la création automatique d’entiers, etc.

Maintenant, la raison s1 et s2 changent de valeur, c’est qu’ils se réfèrent tous deux à la même chaîne interne. Le compilateur fait cela (comme mentionné par d’autres réponses).

La raison pour laquelle s3 ne l’était pas était en fait un peu surprenante pour moi, car je pensais qu’elle partagerait le tableau de value ( c’était le cas dans une version antérieure de Java , avant Java 7u6). Cependant, en regardant le code source de Ssortingng , nous pouvons voir que le tableau de caractères de value pour une sous-chaîne est réellement copié (en utilisant Arrays.copyOfRange(..) ). C’est pourquoi il rest inchangé.

Vous pouvez installer un SecurityManager , afin d’éviter tout code malveillant. Mais gardez à l’esprit que certaines bibliothèques dépendent de l’utilisation de ces astuces de reflection (généralement des outils ORM, des bibliothèques AOP, etc.).

*) Au départ, j’ai écrit que les Ssortingng ne sont pas vraiment immuables, mais simplement “immuables”. Cela peut être trompeur dans l’implémentation actuelle de Ssortingng , où le tableau de value est en effet marqué comme private final . Il est cependant intéressant de noter qu’il n’ya aucun moyen de déclarer un tableau en Java immuable, il faut donc veiller à ne pas l’exposer en dehors de sa classe, même avec les modificateurs d’access appropriés.


Comme ce sujet semble extrêmement populaire, voici quelques suggestions de lectures supplémentaires: Réflexion de folie de Heinz Kabutz sur JavaZone 2009, qui couvre un grand nombre de questions dans l’OP, ainsi que d’autres reflections … enfin … la folie.

Il explique pourquoi cela est parfois utile. Et pourquoi, la plupart du temps, vous devriez l’éviter. 🙂

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

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

initialisation

C’est la raison pour laquelle la comparaison renvoie true. La troisième chaîne est créée à l’aide de subssortingng() qui crée une nouvelle chaîne au lieu de pointer sur la même.

sous-chaîne

Lorsque vous accédez à une chaîne en utilisant la reflection, vous obtenez le pointeur actuel:

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

Donc, changer cela changera la chaîne contenant un pointeur, mais comme s3 est créé avec une nouvelle chaîne à cause de la subssortingng() cela ne changera pas.

changement

Vous utilisez la reflection pour contourner l’immutabilité de Ssortingng – c’est une forme d’attaque.

Vous pouvez créer de nombreux exemples comme cela (par exemple, vous pouvez même instancier un object Void ), mais cela ne signifie pas que Ssortingng n’est pas “immuable”.

Il y a des cas d’utilisation où ce type de code peut être utilisé à votre avantage et être un «bon codage», comme effacer les mots de passe de la mémoire le plus tôt possible (avant le GC) .

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

Vous utilisez la reflection pour accéder aux “détails d’implémentation” de l’object chaîne. Immutabilité est la fonctionnalité de l’interface publique d’un object.

Les modificateurs de visibilité et final (c.-à-d. L’immuabilité) ne sont pas une mesure contre les codes malveillants en Java; ce ne sont que des outils pour se protéger contre les erreurs et rendre le code plus facile à maintenir (l’un des principaux arguments de vente du système). C’est pourquoi vous pouvez accéder aux détails d’implémentation internes comme le tableau de caractères de sauvegarde pour Ssortingng s via la reflection.

Le second effet que vous voyez est que toutes les Ssortingng changent alors qu’il semble que vous ne modifiez que s1 . Les littéraux Java Ssortingng ont une propriété certaine d’être automatiquement internés, c’est-à-dire mis en cache. Deux littéraux de chaîne ayant la même valeur seront en réalité le même object. Lorsque vous créez une chaîne avec new celle-ci ne sera pas automatiquement internée et vous ne verrez pas cet effet.

#subssortingng jusqu’à récemment (Java 7u6) fonctionnait de manière similaire, ce qui aurait expliqué le comportement dans la version originale de votre question. Il n’a pas créé un nouveau tableau de caractères de backing mais a réutilisé celui de la chaîne d’origine; Il a simplement créé un nouvel object Ssortingng utilisant un décalage et une longueur pour ne présenter qu’une partie de ce tableau. Cela fonctionnait généralement comme les chaînes sont immuables – à moins que vous ne contourniez cela. Cette propriété de #subssortingng signifiait également que toute la chaîne d’origine ne pouvait pas être récupérée lorsqu’un sous-programme plus court créé à partir de celui-ci existait toujours.

A partir de Java actuel et de votre version actuelle de la question, il n’y a pas de comportement étrange de #subssortingng .

L’immutabilité des chaînes est du sharepoint vue de l’interface. Vous utilisez la reflection pour contourner l’interface et modifier directement les composants internes des instances Ssortingng.

s1 et s2 sont tous deux modifiés car ils sont tous deux assignés à la même instance de chaîne “intern”. Vous pouvez en apprendre un peu plus sur cet article à propos de l’égalité des chaînes et de l’internement. Vous pourriez être surpris de découvrir que dans votre exemple de code, s1 == s2 renvoie true !

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

Citant la représentation sous forme de chaîne interne à Oracle Tunes Java :

Dans le nouveau paradigme, les champs Ssortingng offset et count ont été supprimés, de sorte que les sous-chaînes ne partagent plus la valeur sous-jacente du caractère [].

Avec ce changement, cela peut arriver sans reflection (???).

Il y a vraiment deux questions ici:

  1. Les cordes sont-elles vraiment immuables?
  2. Pourquoi s3 n’a pas changé?

Au point 1: Excepté pour la ROM, votre ordinateur ne contient aucune mémoire immuable. De nos jours, même la ROM est parfois accessible en écriture. Il y a toujours du code quelque part (que ce soit le kernel ou le code natif qui contourne votre environnement géré) qui peut écrire dans votre adresse mémoire. Donc, dans la “réalité”, non, ils ne sont pas absolument immuables.

Au point 2: Ceci est dû au fait que la sous-chaîne alloue probablement une nouvelle instance de chaîne, ce qui est probablement en train de copier le tableau. Il est possible d’implémenter une sous-chaîne de manière à ce qu’elle ne fasse pas de copie, mais cela ne veut pas dire qu’elle le fait. Il y a des compromis à faire.

Par exemple, si vous tenez une référence à reallyLargeSsortingng.subssortingng(reallyLargeSsortingng.length - 2) , une grande quantité de mémoire peut être conservée, ou seulement quelques octets?

Cela dépend de la manière dont la sous-chaîne est implémentée. Une copie profonde conservera moins de mémoire, mais elle sera légèrement plus lente. Une copie superficielle gardera plus de mémoire en vie, mais ce sera plus rapide. L’utilisation d’une copie profonde peut également réduire la fragmentation du segment de mémoire, car l’object chaîne et son tampon peuvent être alloués dans un bloc, par opposition à deux allocations de segment de mémoire distinctes.

Dans tous les cas, il semble que votre JVM a choisi d’utiliser des copies profondes pour les appels de sous-chaînes.

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

La première chose est une modification d’une chaîne constante stockée dans un pool de chaînes. Lorsque la chaîne est déclarée en tant que Ssortingng s = "Hello World"; , il est placé dans un pool d’objects spéciaux pour une réutilisation potentielle supplémentaire. Le problème est que le compilateur place une référence à la version modifiée au moment de la compilation et une fois que l’utilisateur modifie la chaîne stockée dans ce pool à l’exécution, toutes les références dans le code vont pointer vers la version modifiée. Cela se traduirait par 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 de telles chaînes risquées. Il y a eu un bogue qui s’est produit comme 1 fois sur 1 fois pendant le calcul, ce qui a rendu le résultat indéterminé. J’ai pu trouver le problème en éteignant le JIT – je obtenais toujours le même résultat avec JIT désactivé. Je suppose que la raison en était ce piratage de sécurité Ssortingng qui a cassé certains des contrats d’optimisation JIT.

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

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

Alors maintenant, la valeur de S1 est modifiée (en utilisant la valeur char []). Ainsi, la valeur de l’emplacement de mémoire M1 indiquée 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 rest inchangée, donc s3 contient la même valeur d’origine.

La raison pour laquelle s3 ne change pas est que, dans Java, lorsque vous faites une sous-chaîne, le tableau de valeurs d’une sous-chaîne est copié en interne (avec Arrays.copyOfRange ()).

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

Ssortingng est immuable, mais grâce à la reflection, vous êtes autorisé à modifier la classe Ssortingng. Vous venez de redéfinir la classe Ssortingng comme 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.

[Clause de non-responsabilité: il s’agit d’un style de réponse délibérément motivé, car je me sens plus à l’aise de ne pas faire ça chez les enfants à la maison.]

Le sin est la ligne field.setAccessible(true); qui dit violer le public api en permettant l’access à un domaine 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 des détails d’implémentation que vous ne verriez jamais lorsque vous n’utilisez pas cette ligne de code dangereuse pour violer les modificateurs d’access via la reflection. Il est clair que deux chaînes (normalement) immuables peuvent partager le même tableau de caractères. Si une sous-chaîne partage le même tableau, cela dépend si c’est possible et si le développeur a pensé le partager. Normalement, ce sont des détails d’implémentation invisibles que vous ne devriez pas avoir à connaître à moins de tirer sur le modificateur d’access à travers la tête avec cette ligne de code.

Ce n’est simplement pas une bonne idée de compter sur de tels détails qui ne peuvent pas être expérimentés sans violer les modificateurs d’access en utilisant la reflection. Le propriétaire de cette classe ne prend en charge que l’API publique normale et est libre de modifier l’implémentation à l’avenir.

Après avoir dit que la ligne de code est vraiment très utile lorsque vous avez une arme à feu vous a tenu la tête vous obligeant à faire des choses si dangereuses. L’utilisation de cette porte dérobée est généralement une odeur de code que vous devez mettre à niveau vers un meilleur code de bibliothèque où vous n’avez pas à 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 frameworks (à la fois pour et contre eux), donc je vais éviter d’inviter une guerre de flammes en disant que la grande majorité des programmeurs ne sont pas obligés d’y aller.

Les chaînes sont créées dans la zone permanente de la mémoire du tas JVM. Donc oui, c’est vraiment immuable et on ne peut pas le changer après sa création. 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 object est créé, il entre dans la zone de segment de génération jeune et dans la zone PermGen réservée au pool de chaînes.

Voici plus de détails, vous pouvez aller chercher plus d’informations à partir de: How Garbage Collection fonctionne en Java .