周五晚上,小刘抱着电脑来找我:"杨工,我改一个对象里的列表,结果另一个对象也跟着变了,这是见鬼了吗?"
我笑了:"没见鬼,你这是浅拷贝。"
第一幕:搬家那天
小刘租的房子到期,要搬去新小区。
他有两样东西:
- 一个背包,里面装着身份证、钥匙;
- 一个书架,上面放着几十本书。
搬家师傅问:"怎么搬?"
小刘说:"背包里的东西照原样复制一份;书架太重了,别搬书了,**给我一张纸条,写着'书架在老房子客厅'**就行。"
于是新家里:
- 背包是全新的,身份证、钥匙都是复制品;
- 书架没有搬过来,只有一张纸条,指向老房子的书架。
这时候问题来了。
小刘在新家,从"书架"上抽走一本书。
结果第二天他回老房子一看:老房子的书架上也少了一本书。
他懵了:"我没动老房子啊?"
我说:"你动了。因为你新家根本没有书架,你只是拿纸条找到了老房子的书架。你抽走的,是同一排书。"
第二幕:浅拷贝是什么
浅拷贝就是:外层复制了一份,但里面的引用对象没有复制,仍然指向同一个。
用 Java 来理解:
class Person implements Cloneable {
String name;
List<String> books;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅拷贝
}
}
super.clone() 会复制 name、books 这个引用,但不会复制 books 指向的那个 ArrayList。
所以:
Person p1 = new Person();
p1.name = "小刘";
p1.books = new ArrayList<>();
p1.books.add("Java核心技术");
Person p2 = (Person) p1.clone();
p2.books.add("深入理解JVM");
System.out.println(p1.books); // 也会出现《深入理解JVM》
因为 p1.books 和 p2.books 指向的是同一个列表。
就像新家没有真正的书架,只有一张纸条,指向老房子的书架。
第三幕:深拷贝是什么
小刘说:"那我不想这样,我要新家也有自己的书架。"
我说:"那就得把书也搬过来。"
深拷贝就是:
- 复制外层对象;
- 再递归复制里面所有引用对象;
- 直到所有东西都是独立的副本。
搬家师傅这次说:"行,书架我也给你搬一份,书也照原样复制一份。"
于是新家有了:
- 自己的背包;
- 自己的书架;
- 自己的书。
这时候小刘在新家抽走一本书,老房子完全不受影响。
用代码表示就是:
class Person implements Cloneable {
String name;
List<String> books;
@Override
protected Object clone() throws CloneNotSupportedException {
Person copy = (Person) super.clone();
// 把内部集合也复制一份
copy.books = new ArrayList<>(this.books);
return copy;
}
}
这时候:
Person p1 = new Person();
p1.name = "小刘";
p1.books = new ArrayList<>();
p1.books.add("Java核心技术");
Person p2 = (Person) p1.clone();
p2.books.add("深入理解JVM");
System.out.println(p1.books); // 只有《Java核心技术》
System.out.println(p2.books); // 《Java核心技术》《深入理解JVM》
两个对象彻底独立。
第四幕:一句话分清它们
浅拷贝 :复制了对象外壳,但内部引用还共享。
深拷贝:外壳和内部引用都复制,得到完全独立的副本。
再打个比方:
| 拷贝方式 | 搬家比喻 | 修改内部数据会怎样 |
|---|---|---|
| 浅拷贝 | 新家只拿到一张"书架地址纸条" | 改一个,另一个也跟着变 |
| 深拷贝 | 新家搬来一整套新书架和新书 | 两个互不影响 |
第五幕:Java 里常见的深拷贝方式
小刘问:"那实际开发里怎么深拷贝?"
我说:"常见有几种。"
-
手动 new + 赋值
最朴素,也最可控。
-
重写
clone,逐层复制适合结构不复杂的对象。
-
序列化反序列化
比如用 JSON:
String json = JSON.toJSONString(p1); Person p2 = JSON.parseObject(json, Person.class);只要对象能序列化,就能一次性深拷贝。
-
构造器拷贝
给对象写一个拷贝构造函数:
public Person(Person other) { this.name = other.name; this.books = new ArrayList<>(other.books); }
尾声
小刘合上电脑:"所以浅拷贝是'复制了指针',深拷贝是'复制了指针指向的东西'。"
我点点头:"再补一句:浅拷贝快,但可能共享状态;深拷贝安全,但成本高。 如果对象里有集合、嵌套对象、可变引用,千万别以为复制一份就安全了。"
他想了想:"那我以后看到 clone(),第一反应不是'复制好了',而是'里面有没有共享引用'。"
我说:"这就对了。浅拷贝复制的是门牌号,深拷贝复制的是整栋房子。"