我们都知道,在Java中,String是一个引用类型,我们常说的String不可变,其实指的是字符串对象的内容不可变,也就是其内存中的内容不能被修改,我们平时修改的只是对这块内存的引用,而普通的对象(比如User)修改后,内存中的内容是可以变化的。
代码语言:javascript复制String s = "abc";
s = "123"; // 这里只是修改了引用,实际上创建了两个对象那么,String内容的不可变具体是如何实现的呢?
截取了部分源码,可以看出String本身是基于一个字符类型的数组实现的(JDK8之前是char,JDK9后是byte类型),其中有两个final关键字,一个修饰了String类本身,一个修饰了字符数组。
我们都知道,final用来修饰引用类型时,表示引用的指向不变,也就是说byte不能再指向另外一个数组。
代码语言:javascript复制public final class String
implements java.io.Serializable, Comparable
Constable, ConstantDesc {
@Stable
private final byte[] value;说到这里,其实也不能解释为什么内容是不可变的,因为Java 数组是对象,哪怕它被 final 修饰,也只是表示引用不变,但数组里的元素还是可以改的。
那么在 String 中,是如何做到连数组内容也不允许修改的?有以下几个机制:
一、从外部来说,value 是 private final 修饰的
private保证外部根本访问不到这个字段,而且没有任何的get方法,外部拿不到这个数组,也就没有修改的机会
二、从内部来说,String不提供任何写方法
String不提供任何set方法,没有任何可以修改内部属性的方式,一但创建后其内容也永远不可变
三、 构造时使用防御性拷贝(defensive copy)
在构造方法中传入内容时,不是直接使用this赋值给内部字段,而是先把内容拷贝一份,避免外部篡改原数组
代码语言:javascript复制public String(char[] value) {
this.value = Arrays.copyOf(value, value.length);
}不使用防御性拷贝的后果:
代码语言:javascript复制public class ImmutableStudent {
private final List
public ImmutableStudent(List
// 错误示范:直接this赋值,没有防御性拷贝
this.courses = courses;
}
public List
return courses;
}
@Override
public String toString(){
return courses.toString();
}
}为对象传参:
代码语言:javascript复制public class TestImmutable {
public static void main(String[] args) {
List
list.add("数学");
list.add("语文");
list.add("英语");
ImmutableStudent immutableStudent = new ImmutableStudent(list);
System.out.println(immutableStudent.toString());
list.add("物理"); // 外部进行修改
System.out.println(immutableStudent.toString());
}
}
输出:[数学, 语文, 英语]
[数学, 语文, 英语, 物理]可以看出两次打印的结果不一致,原因是外部通过原始的引用修改了对象的内容,导致共享副本的风险