今天看到一篇文章,问题是《String 真的是不可变的吗?》,题主最后通过反射机制修改了String 内部的char[] 对象的值,最终完成了String 的修改。

这里先上原博主的示例:



// 创建字符串"Hello World", 并赋给引用s
String s = "Hello World";

System.out.println("s = " + s); // Hello World

// 获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");

// 改变value属性的访问权限
valueFieldOfString.setAccessible(true);

// 获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);

// 改变value所引用的数组中的第5个字符
value[5] = '_';

System.out.println("s = " + s); // Hello_World



运行结果



s = Hello World
s = Hello_World



看完这个问题,我的第一反应是,修改完成之后全局字符创常量池(StringPool)中的字符串引用数是一个还是两个,堆中存在的字符串实例是一个还是两个,然后我向验证我的想法。

验证代码



public static void main(String[] args) throws Exception {
    // 创建字符串"Hello World", 并赋给引用s
    String s1 = "Hello World";
    String s2 = "Hello World";

    System.out.println("s1 = " + s1); // Hello World
    System.out.println("s2 = " + s2); // Hello World
    System.out.println("s1 == s2 : " + (s1 == s2)); // true
    System.out.println("s1 equals s2 : " + s1.equals(s2)); // true
    System.out.println();

    // 获取String类中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");

    // 改变value属性的访问权限
    valueFieldOfString.setAccessible(true);

    // 获取s对象上的value属性的值
    char[] value = (char[]) valueFieldOfString.get(s1);

    // 改变value所引用的数组中的第5个字符
    value[5] = '_';

    System.out.println("s1 = " + s1); // Hello_World
    System.out.println("s2 = " + s2); // Hello_World
    System.out.println("s1 == s2 : " + (s1 == s2)); // true
    System.out.println("s1 equals s2 : " + s1.equals(s2)); // true
    System.out.println();

    String s3 = "Hello World";
    System.out.println("s3 = " + s3); // Hello_World
    System.out.println("s1 == s3 : " + (s1 == s3)); // true
    System.out.println("s1 equals s3 : " + s1.equals(s3)); // true
    System.out.println();


    String s4 = "Hello_World";
    System.out.println("s4 = " + s4); // Hello_World
    System.out.println("s1 == s4 : " + (s1 == s4)); // false
    System.out.println("s1 equals s4 : " + s1.equals(s4)); // true
}



运行结果



s1 = Hello World
s2 = Hello World
s1 == s2 : true
s1 equals s2 : true

s1 = Hello_World
s2 = Hello_World
s1 == s2 : true
s1 equals s2 : true

s3 = Hello_World
s1 == s3 : true
s1 equals s3 : true

s4 = Hello_World
s1 == s4 : false
s1 equals s4 : true



对于这个结果,我提出了我的想法(猜想)




java 反射修改变量类型 反射修改string_System


首先第一行代码


String s1 = "Hello World";


JVM 会现在全局字符串常量池中查找是否已经有了这个字符串实例(常量共享),如果没有则会在堆中创建"Hello World" 的字符串实例,然后在全局字符串常量池中创建其引用,全局字符串常量池通过一个StringTable 对象来存储这个引用关系,简单猜想就是以key-value 的格式存储,即StringHashCode-String实例引用的方式。最后s1 指向全局字符串常量池的引用。

紧接着是第二行代码


String s2 = "Hello World";


JVM 通过在全局字符串常量池中查询到了已经存在该字符串实例然后直接返回该实例引用,所以s1 == s2 这个在意料之中,此时堆和字符串常量池也都只有这个字符串。

接下来就是反射修改String 的过程,通过反射获取到s1 的字符串存储对象(char[])并对其进行修改,由于s1、s2 指向的都是相同的实例,所以s1 的修改就等价于s2 的修改,所以修改完成之后,s1 == s2 依然成立,但是堆中的字符串实例已经发生改变,并且旧的实例"Hello World" 已经不存在。

再然后就是


String s3 = "Hello World";


这一步创建的时候,JVM 通过在全局字符串常量池中查询,发现已经有存在的StringHashCode,则直接返回对应的字符串实例,但是该字符串实例已经在上一步被修改成了"Hello_World",这也就导致s1 == s3 不成立。

最后就是


String s4 = "Hello_World"


JVM 在全局字符串常量池中查询,发现没有字符串实例,StringHashCode 不存在,则在堆中创建中字符串实例,并在全局字符串常量池中存储改关系,所以这一步完成之后,存在两个相同但不相等字符串对象实例"Hello_World",一个是通过"Hello World"修改而来的,另一个就是s4 创建的"Hello_World"。

还有一个猜想是Class 常量池,即在静态编译时,s1、s2 编译到到class 文件中,而s3 是运行时动态编译的,所以导致了该结果。

但是最重要的我想要验证这两个猜想,但不知道如何验证。