Une chaîne Java est-elle vraiment immuable?


399

Nous savons tous que cela Stringest 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 ainsi? Et pourquoi la valeur de s1et a-t-elle s2changé, mais pas s3?


394
Vous pouvez faire toutes sortes de trucs stupides avec réflexion. Mais vous cassez fondamentalement l'autocollant «annulation de la garantie si retiré» sur la classe à l'instant où vous le faites.
cHao

16
@DarshanPatel utilise un SecurityManager pour désactiver la réflexion
Sean Patrick Floyd

39
Si vous voulez vraiment jouer avec des choses, vous pouvez le faire (Integer)1+(Integer)2=42en jouant avec la mise en cache automatique; (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )
Richard Tingle

15
Vous pourriez être amusé par cette réponse que j'ai écrite il y a près de 5 ans stackoverflow.com/a/1232332/27423 - il s'agit de listes immuables en C # mais c'est essentiellement la même chose: comment puis-je empêcher les utilisateurs de modifier mes données? Et la réponse est, vous ne pouvez pas; la réflexion le rend très facile. Un langage courant qui n'a pas ce problème est JavaScript, car il n'a pas de système de réflexion qui peut accéder aux variables locales à l'intérieur d'une fermeture, donc privé signifie vraiment privé (même s'il n'y a pas de mot-clé pour cela!)
Daniel Earwicker

49
Quelqu'un lit-il la question jusqu'au bout? La question est, permettez-moi de répéter: "Pourquoi ce programme fonctionne-t-il ainsi? Pourquoi la valeur de s1 et s2 est-elle modifiée et non pas modifiée pour s3?" La question n'est PAS pourquoi les s1 et s2 sont-ils modifiés! La question EST: POURQUOI le s3 n'est-il pas changé?
Roland Pihlakas

Réponses:


403

String est immuable * mais cela signifie uniquement que vous ne pouvez pas le modifier à l'aide de 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, modifier la table de recherche utilisée dans la zone de saisie automatique entière, etc.

Maintenant, la raison s1et la s2valeur de changement, c'est qu'ils se réfèrent tous deux à la même chaîne internée. Le compilateur fait cela (comme mentionné dans d'autres réponses).

La raison s3n'est pas vraiment un peu surprenante pour moi, car je pensais que cela partagerait le valuetableau ( c'était le cas dans la version précédente de Java , avant Java 7u6). Cependant, en regardant le code source de String, nous pouvons voir que le valuetableau de caractères pour une sous-chaîne est réellement copié (en utilisant Arrays.copyOfRange(..)). C'est pourquoi cela reste inchangé.

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

*) J'ai initialement écrit que les Strings ne sont pas vraiment immuables, juste "efficaces immuables". Cela peut être trompeur dans l'implémentation actuelle de String, où le valuetableau est en effet marqué private final. Cependant, il convient de noter qu'il n'y a 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'accès appropriés.


Comme ce sujet semble extrêmement populaire, voici quelques suggestions de lecture supplémentaire: le discours de réflexion sur la folie de Heinz Kabutz de JavaZone 2009, qui couvre beaucoup de questions dans le PO, ainsi que d'autres réflexions ... eh bien ... la folie.

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


7
En fait, l' Stringinternement fait partie du JLS ( "un littéral de chaîne fait toujours référence à la même instance de la classe String" ). Mais je suis d'accord, ce n'est pas une bonne pratique de compter sur les détails d'implémentation de la Stringclasse.
haraldK

3
Peut-être que la raison pour laquelle les substringcopies plutôt que d'utiliser une "section" du tableau existant, est sinon si j'avais une énorme chaîne set en ai retiré une minuscule sous-chaîne appelée t, et j'ai ensuite abandonné smais conservé t, alors le grand tableau serait maintenu en vie (pas de déchets ramassés). Alors peut-être qu'il est plus naturel que chaque valeur de chaîne ait son propre tableau associé?
Jeppe Stig Nielsen

10
Le partage de tableaux entre une chaîne et ses sous-chaînes impliquait également que chaque String instance devait transporter des variables pour se souvenir du décalage dans le tableau et la longueur référencés. C'est une surcharge à ne pas ignorer étant donné le nombre total de chaînes et le rapport typique entre les chaînes normales et les sous-chaînes dans une application. Puisqu'ils devaient être évalués pour chaque opération de chaîne, cela signifiait ralentir chaque opération de chaîne uniquement pour le bénéfice d'une seule opération, une sous-chaîne bon marché.
Holger

2
@Holger - Oui, je crois comprendre que le champ de décalage a été supprimé dans les JVM récentes. Et même lorsqu'elle était présente, elle n'était pas utilisée très souvent.
Hot Licks

2
@supercat: peu importe que vous ayez du code natif ou non, avoir différentes implémentations pour les chaînes et les sous-chaînes dans la même JVM ou avoir des byte[]chaînes pour les chaînes ASCII et char[]pour d'autres implique que chaque opération doit vérifier de quel type de chaîne il s'agit avant en fonctionnement. Cela empêche l'inclusion du code dans les méthodes à l'aide de chaînes, ce qui est la première étape d'optimisations supplémentaires utilisant les informations de contexte de l'appelant. C'est un gros impact.
Holger

93

En Java, si deux variables primitives de chaîne sont initialisées sur le même littéral, il attribue la même référence aux deux variables:

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

initialisation

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

sous-chaîne

Lorsque vous accédez à une chaîne à l'aide de la réflexion, vous obtenez le pointeur réel:

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

Donc, changer cela changera la chaîne contenant un pointeur, mais comme cela s3est créé avec une nouvelle chaîne, substring()cela ne changera pas.

changement


Cela ne fonctionne que pour les littéraux et est une optimisation au moment de la compilation.
SpacePrez

2
@ Zaphod42 Pas vrai. Vous pouvez également appeler internmanuellement sur une chaîne non littérale et profiter des avantages.
Chris Hayes

Attention cependant: vous souhaitez utiliser internjudicieusement. Tout interner ne vous rapporte pas grand-chose et peut être la source de moments de grattage de tête lorsque vous ajoutez de la réflexion au mixage.
cHao

Test1et Test1sont incompatibles avec test1==test2et ne respectent pas les conventions de dénomination java.
c0der

50

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

Il existe de nombreux exemples que vous pouvez créer comme ceci (par exemple, vous pouvez même instancier un Voidobjet 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 être un "bon codage", comme la suppression des mots de passe de la mémoire le plus tôt possible (avant GC) .

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


30

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


24

Les modificateurs de visibilité et final (c'est-à-dire l'immuabilité) ne sont pas une mesure par rapport au code malveillant en Java; ce ne sont que des outils pour se protéger contre les erreurs et pour rendre le code plus maintenable (l'un des gros 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 support pour Strings via la réflexion.

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

#substringjusqu'à 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éé de nouveau tableau de caractères de support 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 ne présenter qu'une partie de ce tableau. Cela a généralement fonctionné car les cordes sont immuables - à moins que vous ne les contourniez. Cette propriété de #substringsignifiait également que l'intégralité de la chaîne d'origine ne pouvait pas être récupérée lorsque une sous-chaîne plus courte créée à partir d'elle existait toujours.

Depuis Java actuel et votre version actuelle de la question, il n'y a pas de comportement étrange #substring.


2
En fait, les modificateurs de visibilité sont (ou du moins étaient) destinés à protéger le code malveillant - cependant, vous devez définir un SecurityManager (System.setSecurityManager ()) pour activer la protection. Comment est-ce vraiment sécurisé est une autre question ...
sleske

2
Mérite un vote positif, car vous insistez sur le fait que les modificateurs d'accès ne sont pas destinés à «protéger» le code. Cela semble être largement mal compris à la fois en Java et en .NET. Bien que le commentaire précédent contredit cela; Je ne connais pas grand chose à Java, mais en .NET c'est certainement vrai. Dans aucune des deux langues, les utilisateurs ne doivent supposer que cela rend leur code indéformable.
Tom W

Il n'est pas possible de violer le contrat de finalmême par la réflexion. En outre, comme mentionné dans une autre réponse, depuis Java 7u6, #substringne partage pas de tableaux.
ntoskrnl

En fait, le comportement de finala changé au fil du temps ...: -O Selon le discours "Reflection Madness" de Heinz que j'ai posté dans l'autre fil, finalsignifiait final dans JDK 1.1, 1.3 et 1.4, mais pourrait être modifié en utilisant la réflexion en utilisant 1.2 toujours , et dans 1.5 et 6 dans la plupart des cas ...
haraldK

1
finalles champs peuvent être modifiés via du nativecode comme le fait le framework de sérialisation lors de la lecture des champs d'une instance sérialisée ainsi que System.setOut(…)qui modifie la System.outvariable finale . Ce dernier est la caractéristique la plus intéressante car la réflexion avec dérogation d'accès ne peut pas changer les static finalchamps.
Holger

11

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 internes des instances de chaîne.

s1et s2sont tous deux modifiés car ils sont tous deux affectés à la même instance de chaîne "interne". Vous pouvez en savoir un peu plus sur cette partie de cet article sur l'égalité des chaînes et l'internement. Vous pourriez être surpris de découvrir que dans votre exemple de code, les s1 == s2retours true!


10

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.

Citant de la représentation de chaîne interne d' Oracle Tunes Java :

Dans le nouveau paradigme, les champs String offset et count 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 (???).


2
Si l'OP utilisait un ancien JRE Sun / Oracle, la dernière instruction afficherait "Java!" (comme il l'a accidentellement posté). Cela n'affecte que le partage du tableau de valeurs entre les chaînes et les sous-chaînes. Vous ne pouvez toujours pas changer la valeur sans astuces, comme la réflexion.
haraldK

7

Il y a vraiment deux questions ici:

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

Point 1: à l'exception de la ROM, il n'y a pas de mémoire immuable dans votre ordinateur. De nos jours, même la ROM est parfois accessible en écriture. Il y a toujours du code quelque part (que ce soit le noyau 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.

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 manière qu'elle ne fasse pas de copie, mais cela ne signifie pas qu'elle le fait. Il y a des compromis à faire.

Par exemple, la détention d'une référence doit-elle maintenir une reallyLargeString.substring(reallyLargeString.length - 2)grande quantité de mémoire en vie ou seulement quelques octets?

Cela dépend de la façon dont la sous-chaîne est implémentée. Une copie complète gardera moins de mémoire en vie, mais elle s'exécutera légèrement plus lentement. Une copie superficielle gardera plus de mémoire en vie, mais ce sera plus rapide. L'utilisation d'une copie complète peut également réduire la fragmentation du segment de mémoire, car l'objet chaîne et son tampon peuvent être alloués en un seul bloc, par opposition à 2 allocations de segment distinctes.

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


3
La vraie ROM est tout aussi immuable qu'un tirage photographique emballé dans du plastique. Le motif est défini de façon permanente lorsque la tranche (ou l'impression) est développée chimiquement. Les mémoires modifiables électriquement, y compris les puces RAM , peuvent se comporter comme une "vraie" ROM si les signaux de commande nécessaires pour l'écrire ne peuvent pas être excités sans ajouter des connexions électriques supplémentaires au circuit dans lequel il est installé. En fait, il n'est pas rare que les appareils intégrés incluent de la RAM qui est définie en usine et maintenue par une batterie de secours, et dont le contenu devrait être rechargé par l'usine en cas de défaillance de la batterie.
supercat

3
@supercat: votre ordinateur n'est cependant pas un de ces systèmes embarqués. :) Les vraies ROM câblées ne sont plus courantes sur les PC depuis une décennie ou deux; EEPROM de tout et flash ces jours-ci. Fondamentalement, chaque adresse visible par l'utilisateur qui fait référence à la mémoire fait référence à la mémoire potentiellement accessible en écriture.
cHao

@cHao: De nombreuses puces flash permettent de protéger des parties en écriture d'une manière qui, si elle peut être annulée, nécessiterait d'appliquer des tensions différentes de celles qui seraient requises pour un fonctionnement normal (ce que les cartes mères ne seraient pas équipées pour le faire). Je m'attendrais à ce que les cartes mères utilisent cette fonctionnalité. De plus, je ne suis pas certain des ordinateurs d'aujourd'hui, mais historiquement, certains ordinateurs avaient une région de RAM qui était protégée en écriture pendant la phase de démarrage et ne pouvait être protégée par une réinitialisation (ce qui forcerait l'exécution à démarrer à partir de la ROM).
supercat

2
@supercat Je pense que vous manquez le point du sujet, à savoir que les chaînes, stockées dans la RAM, ne seront jamais vraiment immuables.
Scott Wisniewski

5

Pour ajouter à la réponse de @ haraldK - il s'agit d'un hack de sécurité qui pourrait entraîner 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 String s = "Hello World";, elle est placée dans un pool d'objets spécial pour une réutilisation potentielle supplémentaire. Le problème est que le compilateur placera 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 au moment de l'exécution, toutes les références dans le code pointeront vers la version modifiée. Cela entraînerait un bogue suivant:

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

Imprime:

Hello Java!

Il y a eu un autre problème que j'ai rencontré lorsque j'implémentais un calcul lourd sur de telles chaînes risquées. Il y a eu un bug qui s'est produit comme 1 fois sur 1000000 pendant le calcul, ce qui a rendu le résultat non déterministe. J'ai pu trouver le problème en éteignant le JIT - J'obtenais toujours le même résultat avec JIT éteint. Je suppose que la raison en est ce hack de sécurité String qui a rompu certains des contrats d'optimisation JIT.


Il s'agissait peut-être d'un problème de sécurité des threads masqué par un temps d'exécution plus lent et une concurrence moindre sans JIT.
Ted Pennings du

@TedPennings D'après ma description, je ne voulais tout simplement pas trop entrer dans les détails. En fait, j'ai passé quelques jours à essayer de le localiser. Il s'agissait d'un algorithme monothread qui calculait une distance entre deux textes écrits dans deux langues différentes. J'ai trouvé deux correctifs possibles pour le problème - l'un était de désactiver le JIT et le second était d'ajouter littéralement aucun op String.format("")dans l'une des boucles internes. Il y a une chance que ce soit un autre problème que JIT, mais je pense que c'était JIT, car ce problème n'a jamais été reproduit après l'ajout de ce no-op.
Andrey Chaschev

Je faisais cela avec une première version de JDK ~ 7u9, donc ça pourrait être ça.
Andrey Chaschev

1
@Andrey Chaschev: «J'ai trouvé deux correctifs possibles pour le problème»… le troisième correctif possible, pour ne pas pirater les Stringinternes, ne vous est pas venu à l'esprit?
Holger

1
@Ted Pennings: les problèmes de sécurité des threads et les problèmes JIT sont souvent les mêmes. Le JIT est autorisé à générer du code qui repose sur les finalgaranties de sécurité des threads de champ qui se cassent lors de la modification des données après la construction de l'objet. Vous pouvez donc le voir comme un problème JIT ou un problème MT comme vous le souhaitez. Le vrai problème est de pirater Stringet de modifier les données qui devraient être immuables.
Holger

5

Selon le concept de regroupement, toutes les variables String contenant la même valeur pointeront vers 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 (disons M1).

D'un autre côté, 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 pointé à 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 s2.

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


5

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

s1 et s2 sont identiques car en Java ils font tous deux référence à la même chaîne interne. C'est par conception en Java.


2
Comment cette réponse a-t-elle ajouté quoi que ce soit aux réponses que vous avez devant vous?
Gray

Notez également qu'il s'agit d'un comportement assez nouveau et qui n'est garanti par aucune spécification.
Paŭlo Ebermann

L'implémentation de String.substring(int, int)changé avec Java 7u6. Avant 7u6, la machine virtuelle Java serait tout simplement garder un pointeur vers l'original Stringest en char[]même temps avec un indice et la longueur. Après 7u6, il copie la sous-chaîne dans un nouveau. StringIl y a des avantages et des inconvénients.
Eric Jablow

2

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


2
Si vous modifiez la visibilité des champs / méthodes, cela n'est pas utile car au moment de la compilation, ils sont privés
Bohème

1
Vous pouvez modifier l'accessibilité des méthodes, mais vous ne pouvez pas modifier leur statut public / privé et vous ne pouvez pas les rendre statiques.
Gray

1

[Avis de non-responsabilité, il s'agit d'un style de réponse délibérément subjectif car je pense qu'une réponse plus «ne fais pas ça à la maison les enfants» est justifiée]

Le péché est la ligne field.setAccessible(true); qui dit violer l'API publique en autorisant 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 sont 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'accès via la réflexion. Clairement, deux chaînes (normalement) immuables peuvent partager le même tableau de caractères. Le fait qu'une sous-chaîne partage le même tableau dépend si elle le peut 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 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 lorsque vous avez un pistolet qui vous tient la tête vous obligeant à faire des choses aussi 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 est d'écrire un "framework vaudou" (orm, conteneur d'injection, ...). Beaucoup de gens deviennent religieux au sujet de tels cadres (pour et contre eux), donc j'éviterai d'inviter à une guerre des flammes en ne disant rien d'autre que la grande majorité des programmeurs n'ont pas à y aller.


1

Les chaînes sont créées dans la zone permanente de la mémoire de tas JVM. Alors oui, c'est vraiment immuable et ne peut pas être changé 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 va dans la zone de segment de mémoire de la jeune génération et la zone PermGen réservée au regroupement de chaînes.

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


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.