Zexun Luo

Java String 类真的不可以改变吗?

Shaka / 2020-12-15


对于 java.lang.String 类,我相信很多人印象里都是它不能被改变。通过查看源码,可以知道 String 有一个 final char 数组的变量,这个变量初始化之后便不能重新赋值。虽然这个变量引用的 char 数组的值虽然可以改变,但是String 类中并没有主动修改 这个 char 数组的方法。这就是通常所说 String 类不可变的依据。

然而在学习反射的时候,我产生了一个疑问:我们不可以通过反射修改 String 内部的 char 数组的值吗?答案是可以的,了解 Java 反射的人很容易就做到这点。那为什么还说 String 不可变呢?在思考过后,我得出一个答案:String 的不可变说的是一种约束规范。换句话说,String 的不可变是有好处的。在实际工作中,代码是自己写来用的,没有人会费劲给自己找麻烦,而没有任何益处。

事情到这里就完了?不,最近我再去了解 String 、String Pool(字符串常量池)的时候,又有了一些疑惑,于是亲手操刀给String 来了一次手术。最后也是加深了对 String 的理解,也验证了上面的观点。先上代码(如果你对任一实验结果感到疑惑,没关系,请带着疑惑来看后面的对照实验):

    //实验1
    String s1 = "1";
    Field valueField = String.class.getDeclaredField("value");
    valueField.setAccessible(true);
    char[] value = (char[]) valueField.get(s1);
    value[0] = '2';
    System.out.println(s1);                 //2
    System.out.println((s1 == "1"));        //true

上述实验 1 做的事情就是:用 s1 = "1" 的方式创建字符串,用反射修改它的值为 "2",最后用它与 "1" 比较,结果是 true 。琢磨琢磨,再看下一段代码:

    //实验2
    String s2 = new String("1");
    Field valueField = String.class.getDeclaredField("value");
    valueField.setAccessible(true);
    char[] value2 = (char[]) valueField.get(s2);
    value2[0] = '2';
    System.out.println(s2);                //2
    System.out.println((s2 == "1"));       //false

上述实验 2 和实验 1 唯一的区别是字符串创建方式,实验2是通过 new String("1") 方式创建,实验1是通过 = "1" 的方式创建。而实验2的结果: s2 == "1"false

到这里,可以停一会了。很多技术文章中都做过类似上面的实验(通过比较创建方式的区别、反射修改等),来分析 String 、String Pool 的原理和设计理念。但我总觉得以此说明最终的结论还不够严谨、或者说还不够直观。接下来请看——魔鬼实验:

    //实验3 
    String s1 = "1";
    Field valueField = String.class.getDeclaredField("value");
    valueField.setAccessible(true);
    char[] value = (char[]) valueField.get(s1);
    value[0] = '2';
    System.out.println(s1);                 //2
    System.out.println((s1 == "1"));        //true
    System.out.println("1");                //2

不难发现,实验3仅仅只是在实验1的基础上输出了 "1" 的值。然而 "1" 输出的值为 2,这样的结果,也许有人会困惑,没关系,看完几个对比实验,相信你会对相关理论(String 、String Pool 等)有了更直观的认识。接下来,请看终极实验:

    //实验4
    String s2 = new String("1");
    Field valueField = String.class.getDeclaredField("value");
    valueField.setAccessible(true);
    char[] value2 = (char[]) valueField.get(s2);
    value2[0] = '2';
    System.out.println(s2);                 //2
    System.out.println((s2 == "1"));        //false
    System.out.println("1");                //2

同样的,上述实验 4 只是在实验 2 的基础上输出了 "1" 的值。结果 "1" 的值还是 2。

结论:
基于对 String 类和字符串常量池(String Pool) 的了解,我对以上实验作出解释(详细理论下回贴出):

实验 3 中 字符串 s1 通过直接赋值字符串常量 "1" 的方式创建,因此 s1 会直接引用字符串常量池中 "1" 对应的对象。对 s1 的修改,也就是对常量池中 "1" 对象的修改,因此 s1"1" 的值都是修改后的 2,而 s1 == "1" 也是 true,因为它们是同一个对象的引用。

实验 4 中 字符串 s2 通过 new String("1")的方式创建,因此,s2 会被新建在堆中,并且,s2char 数组变量会直接引用字符串常量池中 “1"对象的 char 数组,也就是说虽然 s2 是新创建的对象,但是 s2 里存储字符的 char 数组是创建时传进来的字符串常量 "1"char 数组。因此,s2 修改 char 数组,"1" 的也会变。s2"1" 的值都是修改后的 2 。而 s2 == "1"false 的,因为它们引用的不是同一个对象。

最终结论,String 类真的是“不可以”变的!因为这不仅仅是它的特点,更是它的设计初衷。(任何想通过反射修改它的人都应该被拉去祭天)

如果你发现任何问题,请联系我。