web-dev-qa-db-fra.com

Comment cela fonctionne Java? (Pool de chaînes et réflexion)

Le pool de chaînes Java couplé à la réflexion peut produire un résultat inimaginable en Java:

import Java.lang.reflect.Field;

class MessingWithString {
    public static void main (String[] args) {
        String str = "Mario";
        toLuigi(str);
        System.out.println(str + " " + "Mario");
    }

    public static void toLuigi(String original) {
        try {
            Field stringValue = String.class.getDeclaredField("value");
            stringValue.setAccessible(true);
            stringValue.set(original, "Luigi".toCharArray());
        } catch (Exception ex) {
            // Ignore exceptions
        }
    }
}

Le code ci-dessus imprimera:

"Luigi Luigi" 

Qu'est-il arrivé à Mario?

82
macrog

Qu'est-il arrivé à Mario ??

Vous l'avez changé, fondamentalement. Oui, avec la réflexion, vous pouvez violer l'immuabilité des chaînes ... et en raison de l'internement des chaînes, cela signifie que toute utilisation de "Mario" (autre que dans une expression constante de chaîne plus grande, qui aurait été résolue au moment de la compilation) prendra fin comme "Luigi" dans le reste du programme.

Ce genre de chose explique pourquoi la réflexion nécessite des autorisations de sécurité ...

Notez que l'expression str + " " + "Mario" ne pas effectue une concaténation au moment de la compilation, en raison de l'associativité gauche de +. C'est effectivement (str + " ") + "Mario", c'est pourquoi vous voyez toujours Luigi Luigi. Si vous changez le code en:

System.out.println(str + (" " + "Mario"));

... alors vous verrez Luigi Mario car le compilateur aura interné " Mario" vers une chaîne différente de "Mario".

95
Jon Skeet

Il était réglé sur Luigi. Les chaînes en Java sont immuables; ainsi, le compilateur peut interpréter toutes les mentions de "Mario" en tant que références au même élément de pool de constantes String (en gros, "emplacement mémoire"). Vous avez utilisé la réflexion pour changer cet élément; donc tout "Mario" dans votre code, c'est comme si vous écriviez "Luigi".

24
Amadan

Pour expliquer un peu plus les réponses existantes, jetons un œil à votre code d'octet généré (uniquement la méthode main() ici).

Byte Code

Maintenant, toute modification du contenu de cet emplacement affectera à la fois les références (Et toutes les autres que vous donnez également).

16
Codebender

Les littéraux de chaîne sont stockés dans le pool de chaînes et leur valeur canonique est utilisée. Tous les deux "Mario" les littéraux ne sont pas seulement des chaînes avec la même valeur, ils sont le même objet. La manipulation de l'un d'eux (à l'aide de la réflexion) modifiera "les deux", car ce ne sont que deux références au même objet.

9
Mureinik

Vous venez de changer le String de Pool de constantes de chaîne Mario en Luigi qui était référencé par plusieurs Strings, donc chaque référence littérale Mario est maintenant Luigi .

Field stringValue = String.class.getDeclaredField("value");

Vous avez récupéré le champ char[] Nommé value de la classe String

stringValue.setAccessible(true);

Rendez-le accessible.

stringValue.set(original, "Luigi".toCharArray());

Vous avez modifié le champ originalString en Luigi. Mais l'original est Mario le String littéral et le littéral appartient au pool String et tous sont interné . Ce qui signifie que tous les littéraux qui ont le même contenu se réfèrent à la même adresse mémoire.

String a = "Mario";//Created in String pool
String b = "Mario";//Refers to the same Mario of String pool
a == b//TRUE
//You changed 'a' to Luigi and 'b' don't know that
//'a' has been internally changed and 
//'b' still refers to the same address.

Fondamentalement, vous avez modifié le pool de Mario de String qui a reflété dans tous les champs de référence. Si vous créez StringObject (c'est-à-dire new String("Mario")) au lieu de littéral, vous ne rencontrerez pas ce problème car vous aurez deux Mario différents.

8
CoderCroc

Les autres réponses expliquent correctement ce qui se passe. Je voulais juste ajouter le point que cela ne fonctionne que s'il n'y a pas gestionnaire de sécurité installé. Lors de l'exécution de code à partir de la ligne de commande par défaut, il n'y en a pas, et vous pouvez faire des choses comme ça. Cependant, dans un environnement où le code approuvé est mélangé avec du code non approuvé, tel qu'un serveur d'applications dans un environnement de production ou un sandbox d'applet dans un navigateur, il y aurait généralement un gestionnaire de sécurité présent et vous ne seriez pas autorisé à ce genre de manigances, donc il s'agit moins d'un terrible trou de sécurité qu'il n'y paraît.

5
Pepijn Schmitz

Autre point connexe: vous pouvez utiliser le pool constant pour améliorer les performances des comparaisons de chaînes dans certaines circonstances, en utilisant la méthode String.intern() .

Cette méthode renvoie l'instance de String avec le même contenu que la chaîne sur laquelle elle est invoquée à partir du pool de constantes String, en l'ajoutant si elle n'est pas encore présente. En d'autres termes, après avoir utilisé intern(), toutes les chaînes avec le même contenu sont garanties comme étant la même instance de chaîne les unes que les autres et comme toutes les constantes de chaîne avec ce contenu, ce qui signifie que vous pouvez ensuite utiliser l'opérateur égal (==) Sur eux.

Ceci est juste un exemple qui n'est pas très utile en soi, mais il illustre le point:

class Key {
    Key(String keyComponent) {
        this.keyComponent = keyComponent.intern();
    }

    public boolean equals(Object o) {
        // String comparison using the equals operator allowed due to the
        // intern() in the constructor, which guarantees that all values
        // of keyComponent with the same content will refer to the same
        // instance of String:
        return (o instanceof Key) && (keyComponent == ((Key) o).keyComponent);
    }

    public int hashCode() {
        return keyComponent.hashCode();
    }

    boolean isSpecialCase() {
        // String comparison using equals operator valid due to use of
        // intern() in constructor, which guarantees that any keyComponent
        // with the same contents as the SPECIAL_CASE constant will
        // refer to the same instance of String:
        return keyComponent == SPECIAL_CASE;
    }

    private final String keyComponent;

    private static final String SPECIAL_CASE = "SpecialCase";
}

Cette petite astuce ne vaut pas la peine de concevoir votre code, mais il convient de garder à l'esprit le jour où vous remarquerez qu'un peu plus de vitesse pourrait être augmentée d'un peu de code sensible aux performances en utilisant l'opérateur == sur une chaîne avec une utilisation judicieuse de intern().

3
Pepijn Schmitz