前言
以往我们对String不可变的认识大多都停留在内部有一个final修饰的数组,但是这个说法不太严谨,这篇文章来揭示一下String到底为什么不可变。
一、String为什么不可变
String 不可变的表现就是当我们试图对一个已有的对象 “abcd” 赋值为 “abcde”,String 会新创建一个对象。
public class Test {
public static void main(String[] args) {
String str="abcd";
System.out.println(str.hashCode());
str="abcde";
System.out.println(str.hashCode());
}
}
从下图我们看出str的哈希值发生了变化,即str指向了其他的地址。
即原来的"abcd"没有发生变化,这就是String不可变的体现。
为什么String不可变呢?下面给出一种通用的答案。
String内部用 final 修饰 char 数组,这个数组无法被修改。
这个说法确实说的通,但不太严谨。严谨的说法应该是什么呢?
答:严谨的说法应该是String的不可变不仅仅体现在final上面。
这个数组无法被修改仅仅是指引用地址不可被修改(也就是这个叫 value 的引用地址不可变,编译器不允许我们把 value 指向堆中的另一个地址),并非是value[]这个数组的内容不可修改。
下图尝试将final修饰的引用value指向其他的数组,编译器直接报错,也就是上面说的引用地址不可变。
但是如果我们改变value数组本身的内容,这样操作是可行的。
public class Test {
public static void main(String[] args) {
final int []value={1,2,3};
value[0]=5;
System.out.println(Arrays.toString(value)); //输出[5, 2, 3]
}
}
那看到这里大家可能有点迷惑了,这value数组如果是可变的,那不是乱套了吗?
接下来我们再来回顾一下上面说的严谨的说法:String的不可变不仅仅体现在final上面。
1.value数组是用private修饰的,且String外界不提供修改这个数组的方法,所以初始化之后我们没法继续改变value数组的内容。
2.String 类被 final 修饰的,也就是不可继承,避免被他人继承后破坏。
3.Java 作者在 String 的所有方法里面,都很小心地避免去修改了 char 数组中的数据,涉及到对 char 数组中数据进行修改的操作全部都会重新创建一个 String 对象。你可以随便翻个源码看看来验证这个说法,比如 substring 方法:
总结:虽然理论上数组内容可变,但是你没有办法改变,这也是不可变的一种体现。
二、为什么要设计成不可变的
1.字符串常量池的需要
字符串常量池的定义:大量频繁的创建字符串,将会极大程度的影响程序的性能。为此,JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:
1.为字符串开辟了一个字符串常量池 String Pool,可以理解为缓存区
2.创建字符串常量时,首先检查字符串常量池中是否存在该字符串
3.若字符串常量池中存在该字符串,则直接返回该引用实例,无需重新实例化;
若不存在,则实例化该字符串并放入池中。
如下面的代码所示,堆内存中只会创建一个 String 对象:
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2) // true
假设 String 允许被改变,那如果我们修改了 str2 的内容为world,那么 str1 也会被修改,这不是我们想要看见的结果。
2.网络连接地址URL,文件路径path通常情况下都是以String类型保存, 假若String不是固定不变的,将会引起各种安全隐患。
3.在Java程序中String类型是使用最多的,这就牵扯到大量的增删改查,每次增删改差之前其实jvm需要检查一下这个String对象的安全性,就是通过hashcode,当设计成不可变对象时候,就保证了每次增删改查的hashcode的唯一性,也就可以放心的操作。
三、如何让String可变
当然,如果你真的很想改变value数组的内容,你也不是不可以。value是私有属性,你只需要一种方法访问类的私有属性即可。
使用反射可以直接修改value数组中的内容,当然建议不要这样做,毕竟JAVA设计者都把String设计成不可变了,你还硬要改变它,这不是为难自己么!
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String str = "HelloWorld";
//获取类的class对象
Class strClass = str.getClass();
//获取class对象的私有属性
Field value = strClass.getDeclaredField("value");
//设置可进入
value.setAccessible(true);
//得到value数组
char[] change = (char[]) value.get(str);
//改变第一个位置的元素值
change[0] = 'I';
System.out.println(str);
}
}
费了半天力气,可以看到我们的value数组已经成功不情愿的被改变了。
总结
如果final修饰的是基本数据类型,则代表其值不可变(基本数据类型存储的就是值),如果final修饰的是引用类型,指的是引用的地址不可变(即必须指向该对象),但是该对象自身内容是可以变得。String不可变不仅仅只是因为final关键字,需要全方面来分析。