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
会被新建在堆中,并且,s2
的 char
数组变量会直接引用字符串常量池中 “1"对象的 char
数组,也就是说虽然 s2
是新创建的对象,但是 s2
里存储字符的 char
数组是创建时传进来的字符串常量 "1"
的 char
数组。因此,s2
修改 char
数组,"1"
的也会变。s2
和 "1"
的值都是修改后的 2 。而 s2 == "1"
是 false
的,因为它们引用的不是同一个对象。
最终结论,String 类真的是“不可以”变的!因为这不仅仅是它的特点,更是它的设计初衷。(任何想通过反射修改它的人都应该被拉去祭天)
如果你发现任何问题,请联系我。