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

这个Java代码段是如何工作的?(字符串池和反射),java,string,reflection,string-pool,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"); }

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" 
马里奥怎么了

马里奥怎么了

你基本上改变了。是的,使用反射可以破坏字符串的不变性。。。由于字符串的内部处理,这意味着任何对“Mario”的使用(除了在更大的字符串常量表达式中,该表达式在编译时会被解析)都会在程序的其余部分变成“Luigi”

这就是反射需要安全权限的原因

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

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

。。。然后,您将看到
Luigi-Mario
,因为编译器将
“Mario”
插入到另一个字符串中,以
“Mario”

它被设置为Luigi。Java中的字符串是不可变的;因此,编译器可以将所有提到的
“Mario”
解释为对同一字符串常量池项的引用(大致为“内存位置”)。您使用反射来更改该项;因此,您代码中的所有
“Mario”
现在就好像您编写的
“Luigi”
字符串文本都存储在字符串池中,并使用它们的规范值。两个
“Mario”
文本不仅仅是具有相同值的字符串,它们是相同的对象。操作其中一个(使用反射)将修改它们的“两个”,因为它们只是对同一个对象的两个引用。

解释现有的答案多一点,让我们来看看您生成的字节码(这里仅是<代码>())/代码>方法。


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

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

Field stringValue = String.class.getDeclaredField("value");
您已经从类
String

stringValue.setAccessible(true);
让它容易接近

stringValue.set(original, "Luigi".toCharArray());
您将
original
String
字段更改为
Luigi
。但是原始版本是
Mario
字符串
literal,literal属于
String
池,并且都是内部的。这意味着所有具有相同内容的文本都指向相同的内存地址

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
池的Mario,它反映在所有引用字段中。如果您创建
String
对象
(即
新字符串(“Mario”)
)而不是literal,您将不会面临此行为,因为您将有两个不同的
Mario

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

另一个相关点:在某些情况下,您可以使用常量池来提高字符串比较的性能

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

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

类密钥{
键(字符串键组件){
this.keyComponent=keyComponent.intern();
}
公共布尔等于(对象o){
//由于
//构造函数中的intern(),它保证所有值
//具有相同内容的keyComponent将引用相同的
//字符串的实例:
返回(o键的instanceof)和&(keyComponent==((键)o).keyComponent);
}
公共int hashCode(){
返回keyComponent.hashCode();
}
布尔isSpecialCase(){
//使用等于运算符进行字符串比较由于使用了
//构造函数中的intern(),它保证任何键组件
//将使用与特殊_CASE常量相同的内容
//请参考字符串的同一实例:
返回键组件==特殊情况;
}
私有最终字符串键组件;
私有静态最终字符串SPECIAL_CASE=“SpecialCase”;
}
这个小技巧不值得设计代码,但值得记住的是,当您注意到在字符串上使用
=
操作符并明智地使用
intern()

时,可以从一些性能敏感的代码中获得更快的速度“除较大的字符串常量表达式外”位可能并非始终100%为真。在这个问题中,
System.out.println
调用使用编译时常量表达式(
“+”Mario“
),但“Mario”的实例仍然会发生更改。我怀疑这是由于优化