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

How does this Java code snippet work? (String pool and reflection)

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" 

马里奥怎么了?

What happened to Mario ??

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

这种事情就是反射需要安全权限的原因...

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

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

... 然后你会看到 Luigi Mario 因为编译器会将 " Mario" 固定到与 "Mario".

不同的字符串

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

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

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

现在,对该位置的内容所做的任何更改都会影响 参考文献(以及您提供的任何其他参考文献)。

你刚刚把字符串常量池MarioString改成了Luigi被多个 String 引用,所以每个引用 文字 Mario 现在是 Luigi.

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

您已从 class String

中获取名为 valuechar[] 字段
stringValue.setAccessible(true);

使其易于访问。

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

您将 original String 字段更改为 Luigi。但是原始的是 Mario String literal 并且 literal 属于 String 池并且都是 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(即 new String("Mario"))而不是文字,您将不会遇到这种行为,因为您将有两个不同的 Marios。

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

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

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

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

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().