86

Java字符串池加上反射可以在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
        }
    }
}

上面的代码将打印:

"Luigi Luigi" 

马里奥怎么了?

4

7 回答 7

98

马里奥怎么了??

你改变了它,基本上。是的,通过反射,您可以违反字符串的不变性......并且由于字符串实习,这意味着“Mario”的任何使用(除了在更大的字符串常量表达式中,这将在编译时解决)将结束在程序的其余部分中作为“Luigi”。

这种事情就是为什么反射需要安全权限......

请注意,由于 的左关联性,该表达式str + " " + "Mario"不执行任何+编译时连接。这是有效的(str + " ") + "Mario",这就是为什么你仍然看到Luigi Luigi。如果您将代码更改为:

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

...然后你会看到Luigi Mario编译器将实习" Mario"到不同的字符串到"Mario".

于 2015-09-17T06:34:28.500 回答
24

它被设置为路易吉。Java 中的字符串是不可变的;因此,编译器可以将所有提及的内容解释"Mario"为对同一字符串常量池项(大致为“内存位置”)的引用。您使用反射来更改该项目;因此"Mario",您的代码中的所有内容现在都如同您编写的"Luigi".

于 2015-09-17T06:35:07.800 回答
16

为了进一步解释现有的答案,让我们看一下您生成的字节码(仅main()此处的方法)。

字节码

现在,对该位置内容的任何更改都会影响两个引用(以及您提供的任何其他引用)。

于 2015-09-17T06:41:18.650 回答
9

字符串文字存储在字符串池中,并使用它们的规范值。两个"Mario"文字不仅仅是具有相同值的字符串,它们是同一个 object。操作其中一个(使用反射)将修改它们中的“两者”,因为它们只是对同一对象的两个引用。

于 2015-09-17T06:35:30.580 回答
8

您刚刚更改了多个 s 引用StringString 常量池 Mario,因此每个引用文字都是 now 。LuigiString MarioLuigi

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

您已从类中获取char[]命名value字段String

stringValue.setAccessible(true);

使其可访问。

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

您将original String 字段更改为Luigi。但是 original 是Mario字面String ,字面量属于Stringpool 并且都是interned。这意味着所有具有相同内容的文字都指的是相同的内存地址。

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.

基本上你已经改变了String池中的马里奥,它反映在所有参考字段中。如果您创建String Object(ie new String("Mario")) 而不是文字,您将不会遇到这种行为,因为您将有两个不同Mario的 s 。

于 2015-09-17T06:50:30.577 回答
5

其他答案充分解释了发生了什么。我只是想补充一点,这仅在没有安装安全管理器的情况下才有效。默认情况下从命令行运行代码时没有,您可以这样做。但是,在受信任代码与不受信任代码混合的环境中,例如生产环境中的应用程序服务器或浏览器中的小程序沙箱,通常会存在安全管理器,并且不允许您进行此类恶作剧,因此这看起来不像是一个可怕的安全漏洞。

于 2015-09-17T10:33:23.573 回答
3

另一个相关点:在某些情况下,您可以利用常量池来提高字符串比较的性能,通过使用String.intern()方法。

该方法返回与从 String 常量池调用它的 String 具有相同内容的 String 实例,如果尚不存在则添加它。换句话说,在 using 之后intern(),所有具有相同内容的 String 都保证彼此是相同的 String 实例,并且与具有这些内容的任何 String 常量一样,这意味着您可以==在它们上使用等号运算符 ( )。

这只是一个例子,它本身并不是很有用,但它说明了这一点:

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";
}

==这个小技巧不值得围绕你的代码设计,但是当你注意到通过在字符串上使用运算符并明智地使用一些性能敏感的代码时,值得记住这一天的intern()

于 2015-09-18T12:48:19.360 回答