面試題:深克隆和淺克隆的實作方式
面試官考察點
考察目的: 深克隆和淺克隆,考察的是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學架構]公眾號,獲取更多精品原創

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/342993.html
標籤:Java
