这个Java代码片段如何工作? (字符串池和反射)

时间:2015-09-17 06:29:43

标签: java string reflection string-pool

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" 

马里奥怎么了?

7 个答案:

答案 0 :(得分:95)

  

马里奥怎么了?

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

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

请注意,由于str + " " + "Mario"的左关联性,表达式+执行而不是执行任何编译时串联。它实际上是(str + " ") + "Mario",这就是您仍然看到Luigi Luigi的原因。如果您将代码更改为:

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

...然后您会看到Luigi Mario,因为编译器会将" Mario"插入到"Mario"的不同字符串。

答案 1 :(得分:24)

它被设定为Luigi。 Java中的字符串是不可变的;因此,编译器可以将"Mario"的所有提及解释为对同一个String常量池项的引用(粗略地说,"内存位置")。您使用反射来更改该项目;因此,代码中的所有"Mario"现在就像您编写"Luigi"一样。

答案 2 :(得分:16)

为了解释现有的答案,让我们看看你生成的字节代码(这里只有main()方法)。

Byte Code

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

答案 3 :(得分:9)

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

答案 4 :(得分:8)

您刚刚将 字符串常量池 String的{​​{1}}更改为多个{{1}引用的Mario } s,所以每个引用文字 Luigi现在都是String

Mario

您已从班级Luigi

中提取了Field stringValue = String.class.getDeclaredField("value"); 名为char[]字段的字段
value

让它可以访问。

String

您已将stringValue.setAccessible(true); stringValue.set(original, "Luigi".toCharArray()); 字段更改为original。但原作是String Luigi 文字,文字属于Mario池,所有实习。这意味着具有相同内容的所有文字都指向相同的内存地址。

String

基本上,您已更改了所有引用字段中反映String池的Mario。如果您创建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

答案 5 :(得分:5)

其他答案充分解释了正在发生的事情。我只是想补充一点,即只有在没有安装security manager的情况下才有效。默认情况下,从命令行运行代码时没有,你可以这样做。但是,在受信任的代码与不受信任的代码混合的环境中,例如生产环境中的应用程序服务器或浏览器中的applet沙箱,通常会有一个安全管理器存在,您不会被允许使用这些类型的恶作剧,所以这似乎不是一个可怕的安全漏洞。

答案 6 :(得分:3)

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

该方法返回String的实例,其内容与从String常量池调用它的String相同,如果尚不存在,则将其添加。换句话说,在使用intern()之后,所有具有相同内容的字符串都保证与彼此相同的String实例以及具有这些内容的任何String常量,这意味着您可以使用equals运算符({{1在他们身上。

这只是一个单独的例子,但它说明了一点:

==

这个小技巧并不值得设计你的代码,但值得注意的是,当你注意到使用{{{{{{{{{{{{{{ 1}}运算符在一个字符串上,明智地使用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"; }