文章來源于公眾號(hào)CodeSheep ,作者CodeSheep
對(duì)象拷貝在我們?nèi)粘懘a的時(shí)候基本上是剛性需求,經(jīng)常遇到,只不過很多人天天忙于寫業(yè)務(wù),忽視了一些細(xì)節(jié)問題和理解,有時(shí)候這方面一旦出了問題,就不太容易排查了。
所以本篇好好梳理一下。
注:本文已收錄于Github開源項(xiàng)目:github.com/hansonwang99/JavaCollection
”
值類型 vs 引用類型
這兩個(gè)概念的準(zhǔn)確區(qū)分,對(duì)于深、淺拷貝問題的理解非常重要。
正如Java圣經(jīng)《Java編程思想》第二章的標(biāo)題所言,在Java中一切都可以視為對(duì)象!
所以來到Java的世界,我們要習(xí)慣用引用去操作對(duì)象。在Java中,像數(shù)組、類Class、枚舉Enum、Integer包裝類等等,就是典型的引用類型,所以操作時(shí)一般來說采用的也是引用傳遞的方式;
但是Java的語言級(jí)基礎(chǔ)數(shù)據(jù)類型,諸如int這些基本類型,操作時(shí)一般采取的則是值傳遞的方式,所以有時(shí)候也稱它為值類型。
為了便于下文的講述和舉例,我們這里先定義兩個(gè)類:Student和Major,分別表示「學(xué)生」以及「所學(xué)的專業(yè)」,二者是包含關(guān)系:
// 學(xué)生的所學(xué)專業(yè)
public class Major {
private String majorName; // 專業(yè)名稱
private long majorId; // 專業(yè)代號(hào)
// ... 其他省略 ...
}
// 學(xué)生
public class Student {
private String name; // 姓名
private int age; // 年齡
private Major major; // 所學(xué)專業(yè)
// ... 其他省略 ...
}
賦值 vs 淺拷貝 vs 深拷貝
對(duì)象賦值
賦值是日常編程過程中最常見的操作,最簡單的比如:
Student codeSheep = new Student();
Student codePig = codeSheep;
嚴(yán)格來說,這種不能算是對(duì)象拷貝,因?yàn)榭截惖膬H僅只是引用關(guān)系,并沒有生成新的實(shí)際對(duì)象:
淺拷貝
淺拷貝屬于對(duì)象克隆方式的一種,重要的特性體現(xiàn)在這個(gè) 「淺」 字上。
比如我們?cè)噲D通過studen1實(shí)例,拷貝得到student2,如果是淺拷貝這種方式,大致模型可以示意成如下所示的樣子:
很明顯,值類型的字段會(huì)復(fù)制一份,而引用類型的字段拷貝的僅僅是引用地址,而該引用地址指向的實(shí)際對(duì)象空間其實(shí)只有一份。
一圖勝前言,我想上面這個(gè)圖已經(jīng)表現(xiàn)得很清楚了。
深拷貝
深拷貝相較于上面所示的淺拷貝,除了值類型字段會(huì)復(fù)制一份,引用類型字段所指向的對(duì)象,會(huì)在內(nèi)存中也創(chuàng)建一個(gè)副本,就像這個(gè)樣子:
原理很清楚明了,下面來看看具體的代碼實(shí)現(xiàn)吧。
淺拷貝代碼實(shí)現(xiàn)
還以上文的例子來講,我想通過student1拷貝得到student2,淺拷貝的典型實(shí)現(xiàn)方式是:讓被復(fù)制對(duì)象的類實(shí)現(xiàn)Cloneable接口,并重寫clone()方法即可。
以上面的Student類拷貝為例:
public class Student implements Cloneable {
private String name; // 姓名
private int age; // 年齡
private Major major; // 所學(xué)專業(yè)
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
// ... 其他省略 ...
}
然后我們寫個(gè)測試代碼,一試便知:
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Major m = new Major("計(jì)算機(jī)科學(xué)與技術(shù)",666666);
Student student1 = new Student( "CodeSheep", 18, m );
// 由 student1 拷貝得到 student2
Student student2 = (Student) student1.clone();
System.out.println( student1 == student2 );
System.out.println( student1 );
System.out.println( student2 );
System.out.println( "\n" );
// 修改student1的值類型字段
student1.setAge( 35 );
// 修改student1的引用類型字段
m.setMajorName( "電子信息工程" );
m.setMajorId( 888888 );
System.out.println( student1 );
System.out.println( student2 );
}
}
運(yùn)行得到如下結(jié)果:
從結(jié)果可以看出:
-
student1==student2打印false,說明clone()方法的確克隆出了一個(gè)新對(duì)象; - 修改值類型字段并不影響克隆出來的新對(duì)象,符合預(yù)期;
- 而修改了
student1內(nèi)部的引用對(duì)象,克隆對(duì)象student2也受到了波及,說明內(nèi)部還是關(guān)聯(lián)在一起的
深拷貝代碼實(shí)現(xiàn)
深度遍歷式拷貝
雖然clone()方法可以完成對(duì)象的拷貝工作,但是注意:clone()方法默認(rèn)是淺拷貝行為,就像上面的例子一樣。若想實(shí)現(xiàn)深拷貝需覆寫 clone()方法實(shí)現(xiàn)引用對(duì)象的深度遍歷式拷貝,進(jìn)行地毯式搜索。
所以對(duì)于上面的例子,如果想實(shí)現(xiàn)深拷貝,首先需要對(duì)更深一層次的引用類Major做改造,讓其也實(shí)現(xiàn)Cloneable接口并重寫clone()方法:
public class Major implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
// ... 其他省略 ...
}
其次我們還需要在頂層的調(diào)用類中重寫clone方法,來調(diào)用引用類型字段的clone()方法實(shí)現(xiàn)深度拷貝,對(duì)應(yīng)到本文那就是Student類:
public class Student implements Cloneable {
@Override
public Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.major = (Major) major.clone(); // 重要?。?!
return student;
}
// ... 其他省略 ...
}
這時(shí)候上面的測試用例不變,運(yùn)行可得結(jié)果:
很明顯,這時(shí)候student1和student2兩個(gè)對(duì)象就完全獨(dú)立了,不受互相的干擾。
利用反序列化實(shí)現(xiàn)深拷貝
利用反序列化技術(shù),我們也可以從一個(gè)對(duì)象深拷貝出另一個(gè)復(fù)制對(duì)象,而且這貨在解決多層套娃式的深拷貝問題時(shí)效果出奇的好。
所以我們這里改造一下Student類,讓其clone()方法通過序列化和反序列化的方式來生成一個(gè)原對(duì)象的深拷貝副本:
public class Student implements Serializable {
private String name; // 姓名
private int age; // 年齡
private Major major; // 所學(xué)專業(yè)
public Student clone() {
try {
// 將對(duì)象本身序列化到字節(jié)流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( byteArrayOutputStream );
objectOutputStream.writeObject( this );
// 再將字節(jié)流通過反序列化方式得到對(duì)象副本
ObjectInputStream objectInputStream =
new ObjectInputStream( new ByteArrayInputStream( byteArrayOutputStream.toByteArray() ) );
return (Student) objectInputStream.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
// ... 其他省略 ...
}
當(dāng)然這種情況下要求被引用的子類(比如這里的Major類)也必須是可以序列化的,即實(shí)現(xiàn)了Serializable接口:
public class Major implements Serializable {
// ... 其他省略 ...
}
這時(shí)候測試用例完全不變,直接運(yùn)行,也可以得到如下結(jié)果:
很明顯,這時(shí)候student1和student2兩個(gè)對(duì)象也是完全獨(dú)立的,不受互相的干擾,深拷貝完成。
后 記
好了,關(guān)于「深拷貝」和「淺拷貝」這個(gè)問題這次就聊到這里吧。本以為這篇會(huì)很快寫完,結(jié)果又扯出了這么多東西,不過這樣一梳理、一串聯(lián),感覺還是清晰了不少。
就這樣吧,下篇見。
注:本文已收錄于Github開源項(xiàng)目:github.com/hansonwang99/JavaCollection
”
每天進(jìn)步一點(diǎn)點(diǎn)
慢一點(diǎn)才能更快