面試題:深克隆和淺克隆的實現方式
面試官考察點
考察目的: 深克隆和淺克隆,考察的是Java基礎知識的理解。
考察人群: 2到5年開發經驗。
背景知識詳解
先了解下淺克隆和深克隆的定義:
- 淺克隆:被復制對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。
- 深克隆:除去那些引用其他對象的變量,被復制對象的所有變量都含有與原來的對象相同的值。那些引用其他對象的變量將指向被復制過的新對象,而不再是原有的那些被引用的對象。換言之,深復制把要復制的對象所引用的對象都復制了一遍。
如何實現克隆
我么先不管深克隆、還是淺克隆。首先,要先了解如何實現克隆,實現克隆需要滿足以下三個步驟
- 對象的類實現Cloneable接口;
- 覆蓋Object類的clone()方法(覆蓋clone()方法,訪問修飾符設為public,默認是protected,但是如果所有類都在同一個包下protected是可以訪問的);
- 在clone()方法中調用super.clone();
實現一個克隆
先定義一個score
類,表示分數信息。
public class Score {
private String category;
private double fraction;
public Score() {
}
public Score(String category, double fraction) {
this.category = category;
this.fraction = fraction;
}
//getter/setter省略
@Override
public String toString() {
return "Score{" +
"category='" + category + '\'' +
", fraction=" + fraction +
'}';
}
}
定義一個Person,其中包含Score
屬性,來表示這個人的考試分數。
需要注意,Person類是實現了Cloneable接口的,並且重寫了clone()
這個方法。
public class Person implements Cloneable{
private String name;
private int age;
private List<Score> score;
public Person() {
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
克隆代碼測試,代碼邏輯不復雜,就是初始化一個對象mic
,然后基於mic
使用clone
方法克隆出一個對象dylan
。
接着通過修改被克隆對象mic
的成員屬性,打印出這兩個對象的狀態信息。
public class CloneMain {
public static void main(String[] args) throws CloneNotSupportedException {
Person mic=new Person();
Score s1=new Score();
s1.setCategory("語文");
s1.setFraction(90);
Score s2=new Score();
s2.setCategory("數學");
s2.setFraction(100);
mic.setAge(18);
mic.setName("Mic");
mic.setScore(Arrays.asList(s1,s2));
System.out.println("person對象初始化狀態:"+mic);
Person dylan=(Person)mic.clone(); //克隆一個對象
System.out.println("打印克隆對象:dylan:"+dylan);
mic.setAge(20);
mic.getScore().get(0).setFraction(70); //修改mic語文分數為70
System.out.println("打印mic:"+mic);
System.out.println("打印dylan:"+dylan);
}
}
執行結果如下:
person對象初始化狀態:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
打印克隆對象:dylan:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
打印mic:Person{name='Mic', age=20, score=[Score{category='語文', fraction=70.0}, Score{category='數學', fraction=100.0}]}
打印dylan:Person{name='Mic', age=18, score=[Score{category='語文', fraction=70.0}, Score{category='數學', fraction=100.0}]}
從結果中可以發現:
- 修改
mic
對象本身的普通屬性age
,發現該屬性的修改只影響到mic
對象本身的實例。 - 當修改
mic
對象的語文成績時,dylan
對象的語文成績也發生了變化。
為什么會導致這個現象?回過頭看一下淺克隆的定義:
淺克隆:創建一個新對象,新對象的屬性和原來對象完全相同,對於非基本類型屬性,仍指向原有屬性所指向的對象的內存地址
需要特別強調
非基本類型
,對於非基本類型,傳遞的是值,所以新的dylan
對象會對該屬性創建一個副本。同樣,對於final
修飾的屬性,由於它的不可變性,在淺克隆時,也會在內存中創建副本。
如圖所示,dylan對象從mic對象克隆過來后,dylan對象的內存地址指向的是同一個。因此當mic
這個對象中的屬性發生變化時,dylan
對象的屬性也會發生變化。
clone方法的源碼分析
經過上述案例演示可以發現,如果對象實現Cloneable並重寫clone方法不進行任何操作時,調用clone是進行的淺克隆,那clone方法是如何實現的呢?它默認情況下做了什么?
clone方法是Object中默認提供的,它的源碼定義如下
protected native Object clone() throws CloneNotSupportedException;
從源碼中我們可以看到幾個關鍵點:
1.clone方法是native方法,native方法的效率遠高於非native方法,因此如果我們需要拷貝一個對象,建議使用clone,而不是new。
2.該方法被protected修飾。這就意味着想要使用,則必須重寫該方法,並且設置成public。
3.返回值是一個Object對象,因此通過clone
方法克隆一個對象,需要強制轉換。
4.如果在沒有實現Cloneable接口的實例上調用Object的clone()方法,則會導致拋出CloneNotSupporteddException;
再來看一下Object.clone方法上的注釋,注釋的內容有點長。
/**
* Creates and returns a copy of this object. The precise meaning
* of "copy" may depend on the class of the object. The general
* intent is that, for any object {@code x}, the expression:
* <blockquote>
* <pre>
* x.clone() != x</pre></blockquote>
* will be true, and that the expression:
* <blockquote>
* <pre>
* x.clone().getClass() == x.getClass()</pre></blockquote>
* will be {@code true}, but these are not absolute requirements.
* While it is typically the case that:
* <blockquote>
* <pre>
* x.clone().equals(x)</pre></blockquote>
* will be {@code true}, this is not an absolute requirement.
* <p>
* By convention, the returned object should be obtained by calling
* {@code super.clone}. If a class and all of its superclasses (except
* {@code Object}) obey this convention, it will be the case that
* {@code x.clone().getClass() == x.getClass()}.
* <p>
* By convention, the object returned by this method should be independent
* of this object (which is being cloned). To achieve this independence,
* it may be necessary to modify one or more fields of the object returned
* by {@code super.clone} before returning it. Typically, this means
* copying any mutable objects that comprise the internal "deep structure"
* of the object being cloned and replacing the references to these
* objects with references to the copies. If a class contains only
* primitive fields or references to immutable objects, then it is usually
* the case that no fields in the object returned by {@code super.clone}
* need to be modified.
* <p>
* The method {@code clone} for class {@code Object} performs a
* specific cloning operation. First, if the class of this object does
* not implement the interface {@code Cloneable}, then a
* {@code CloneNotSupportedException} is thrown. Note that all arrays
* are considered to implement the interface {@code Cloneable} and that
* the return type of the {@code clone} method of an array type {@code T[]}
* is {@code T[]} where T is any reference or primitive type.
* Otherwise, this method creates a new instance of the class of this
* object and initializes all its fields with exactly the contents of
* the corresponding fields of this object, as if by assignment; the
* contents of the fields are not themselves cloned. Thus, this method
* performs a "shallow copy" of this object, not a "deep copy" operation.
* <p>
* The class {@code Object} does not itself implement the interface
* {@code Cloneable}, so calling the {@code clone} method on an object
* whose class is {@code Object} will result in throwing an
* exception at run time.
*
* @return a clone of this instance.
* @throws CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
*/
protected native Object clone() throws CloneNotSupportedException;
上述方法中的注釋描述中,對於clone
方法關於復制描述,提出了三個規則,也就是說,”復制“的確切定義取決於對象本身,它可以滿足以下任意一條規則:
- 對於所有對象,x.clone () !=x 應當返回 true,因為克隆對象與原對象不是同一個對象。
- 對於所有對象,x.clone ().getClass () == x.getClass () 應當返回 true,因為克隆對象與原對象的類型是一樣的。
- 對於所有對象,x.clone ().equals (x) 應當返回 true,因為使用 equals 比較時,它們的值都是相同的。
因此,從clone方法的源碼中可以得到一個結論,clone方法是深克隆還是淺克隆,取決於實現克隆方法對象的本身實現。
深克隆
理解了淺克隆,我們就不難猜測到,所謂深克隆的本質,應該是如下圖所示。
dylan
這個對象實例從mic
對象克隆之后,應該要分配一塊新的內存地址,從而實現在內存地址上的隔離。
深拷貝實現的是對所有可變(沒有被final修飾的引用變量)引用類型的成員變量都開辟獨立的內存空間,使得拷貝對象和被拷貝對象之間彼此獨立,因此一般深拷貝對於淺拷貝來說是比較耗費時間和內存開銷的。
深克隆實現
修改Person類中的clone()
方法,代碼如下。
@Override
protected Object clone() throws CloneNotSupportedException {
Person p=(Person)super.clone(); //可以直接使用clone方法克隆,因為String類型中的屬性是final修飾,而int是基本類型,都會創建副本
if(this.score!=null&&this.score.size()>0){ //如果score不為空時,才做深度克隆
//由於`score`是引用類型,所以需要重新分配內存空間
List<Score> ls=new ArrayList<>();
this.score.stream().forEach(score->{
Score s=new Score();
s.setFraction(score.getFraction());
s.setCategory(score.getCategory());
ls.add(s);
});
p.setScore(ls);
}
return p;
}
再次執行,運行結果如下
person對象初始化狀態:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
打印克隆對象:dylan:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
打印mic:Person{name='Mic', age=20, score=[Score{category='語文', fraction=70.0}, Score{category='數學', fraction=100.0}]}
打印dylan:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
Process finished with exit code 0
從結果可以看到,這兩個對象之間並沒有相互影響,因為我們在clone
方法中,對於Person
這個類的成員屬性Score
使用new
創建了一個新的對象,這樣就使得兩個對象分別指向不同的內存地址。
創建一個新對象,屬性中引用的其他對象也會被克隆,不再指向原有對象地址。總之深淺克隆都會在堆中新分配一塊區域,區別在於對象屬性引用的對象是否需要進行克隆(遞歸性的)
深克隆的其他實現方式
深克隆的實現方式很多,總的來說有以下幾種:
- 所有對象都實現克隆方法。
- 通過構造方法實現深克隆。
- 使用 JDK 自帶的字節流。
- 使用第三方工具實現,比如:Apache Commons Lang。
- 使用 JSON 工具類實現,比如:Gson,FastJSON 等等。
其實,深克隆既然是在內存中創建新的對象,那么任何能夠創建新實例對象的方式都能完成這個動作,因此不局限於這些方法。
所有對象都實現克隆方法
由於淺克隆本質上是因為引用對象指向同一塊內存地址,如果每個對象都實現克隆方法,意味着每個對象的最基本單位是基本數據類型或者封裝類型,而這些類型在克隆時會創建副本,從而避免了指向同一塊內存地址的問題。
修改代碼如下。
public class Person implements Cloneable {
private String name;
private int age;
private List<Score> score;
public Person() {
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person p=(Person)super.clone();
if(this.score!=null&&this.score.size()>0){ //如果score不為空時,才做深度克隆
//由於`score`是引用類型,所以需要重新分配內存空間
List<Score> ls=new ArrayList<>();
this.score.stream().forEach(score->{
try {
ls.add((Score)score.clone()); //這里用了克隆方法
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
});
p.setScore(ls);
}
return p;
}
}
修改Score對象
public class Score implements Cloneable {
private String category;
private double fraction;
public Score() {
}
public Score(String category, double fraction) {
this.category = category;
this.fraction = fraction;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Person dylan=(Person)mic.clone(); //克隆一個對象
運行結果如下
person對象初始化狀態:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
打印克隆對象:dylan:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
打印mic:Person{name='Mic', age=20, score=[Score{category='語文', fraction=70.0}, Score{category='數學', fraction=100.0}]}
打印dylan:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
通過構造方法實現深克隆。
構造方法實現深克隆,其實是我們經常使用的方法,就是使用new
關鍵字來實例化一個新的對象,然后通過構造參數傳值來實現數據拷貝。
public class Person implements Cloneable {
private String name;
private int age;
private List<Score> score;
public Person() {
}
public Person(String name, int age, List<Score> score) {
this.name = name;
this.age = age;
this.score = score;
}
}
克隆的時候,我們這么做
Person dylan=new Person(mic.getName(),mic.getAge(),mic.getScore()); //克隆一個對象
基於ObjectStream實現深克隆
在Java中,對象流也可以實現深克隆,大家可能對對象流這個名詞有點陌生,它的定義如下:
- ObjectOutputStream, 對象輸出流,把一個對象轉換為二進制格式數據
- ObjectInputStream,對象輸入流,把一個二進制數據轉換為對象。
這兩個對象,在Java中通常用來實現對象的序列化。
創建一個工具類,使用ObjectStream來實現對象的克隆,代碼實現邏輯不難:
- 使用ObjectOutputStream,把一個對象轉換為數據流存儲到對象ByteArrayOutputStream中。
- 再從內存中讀取該數據流,使用ObjectInputStream,把該數據流轉換為目標對象。
public class ObjectStreamClone {
public static <T extends Serializable> T clone(T t){
T cloneObj = null;
try {
// bo,存儲對象輸出流,寫入到內存
ByteArrayOutputStream bo = new ByteArrayOutputStream();
//對象輸出流,把對象轉換為數據流
ObjectOutputStream oos = new ObjectOutputStream(bo);
oos.writeObject(t);
oos.close();
// 分配內存,寫入原始對象,生成新對象
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
// 返回生成的新對象
cloneObj = (T) oi.readObject();
oi.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
Person對象和Score對象均需要實現Serializable接口,
public class Person implements Serializable {
}
public class Score implements Serializable {}
修改測試類的克隆方法.
Person dylan=(Person)ObjectStreamClone.clone(mic); //克隆一個對象
運行結果如下:
person對象初始化狀態:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
打印克隆對象:dylan:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
打印mic:Person{name='Mic', age=20, score=[Score{category='語文', fraction=70.0}, Score{category='數學', fraction=100.0}]}
打印dylan:Person{name='Mic', age=18, score=[Score{category='語文', fraction=90.0}, Score{category='數學', fraction=100.0}]}
通過對象流能夠實現深克隆,其根本原因還是在於對象的序列化之后,已經脫離了JVM內存對象的范疇,畢竟一個對象序列化之后,是可以通過文件、或者網絡跨JVM傳輸的,因此對象在反序列化時,必然需要基於該數據流重新反射生成新的對象。
問題解答
問題:深克隆和淺克隆的實現方式
回答:
-
淺克隆是指被復制對象中屬於引用類型的成員變量的內存地址和被克隆對象的內存地址相同,也就是克隆對象只實現了對被克隆對象基本類型的副本克隆。
淺克隆的實現方式,可以實現Cloneable接口,並重寫clone方法,即可完成淺克隆。
淺克隆的好處是,避免了引用對象的內存分配和回收,提高對象的復制效率。
-
深克隆時指實現對於基本類型和引用類型的完整克隆,克隆對象和被克隆對象中的引用對象的內存地址完全隔離。
深克隆的實現方式:
- 基於Cloneable接口重寫clone方法,但是我們需要在clone方法中,針對應用類型的成員變量,使用
new
關鍵字分配獨立的內存空間。 - 基於Java中對象流的方式實現
- 基於構造方法實現深度克隆
- 被克隆的對象中所有涉及到引用類型變量的對象,全部實現克隆方法,並且在被克隆對象的clone方法中,需要調用所有成員對象的clone方法實現對象克隆
- 基於Cloneable接口重寫clone方法,但是我們需要在clone方法中,針對應用類型的成員變量,使用
問題總結
深克隆的本質,其實是保證被克隆對象中所有應用對象以及引用所嵌套的引用對象,全部分配一塊獨立的內存空間,避免克隆對象和被克隆對象指向同一塊內存地址,造成數據錯誤等問題。
所以,深克隆,表示對象拷貝的深度,因為在Java中對象的嵌套是非常常見的。理解了這個知識點,才能避免在開發過程中遇到一些奇奇怪怪的問題。
關注[跟着Mic學架構]公眾號,獲取更多精品原創