[Java提高篇——對象克隆(復制)](http://www.cnblogs.com/Qian123/p/5710533.html)
**閱讀目錄**
* [為什么要克隆?](http://www.cnblogs.com/Qian123/p/5710533.html#_label0)
* [如何實現克隆](http://www.cnblogs.com/Qian123/p/5710533.html#_label1)
* [淺克隆和深克隆](http://www.cnblogs.com/Qian123/p/5710533.html#_label2)
* [解決多層克隆問題](http://www.cnblogs.com/Qian123/p/5710533.html#_label3)
* [總結](http://www.cnblogs.com/Qian123/p/5710533.html#_label4)
假如說你想復制一個簡單變量。很簡單:
~~~
int apples = 5;
int pears = apples;
~~~
不僅僅是int類型,其它七種原始數據類型(boolean,char,byte,short,float,double.long)同樣適用于該類情況。
但是如果你復制的是一個對象,情況就有些復雜了。
假設說我是一個beginner,我會這樣寫:

~~~
class Student {
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = stu1;
System.out.println("學生1:" + stu1.getNumber());
System.out.println("學生2:" + stu2.getNumber());
}
}
~~~

結果:
學生1:12345 ?
學生2:12345 ?
這里我們自定義了一個學生類,該類只有一個number字段。
我們新建了一個學生實例,然后將該值賦值給stu2實例。(Student stu2 = stu1;)
再看看打印結果,作為一個新手,拍了拍胸腹,對象復制不過如此,
難道真的是這樣嗎?
我們試著改變stu2實例的number字段,再打印結果看看:
~~~
stu2.setNumber(54321);
System.out.println("學生1:" + stu1.getNumber());
System.out.println("學生2:" + stu2.getNumber());
~~~
結果:
學生1:54321 ?
學生2:54321 ?
這就怪了,為什么改變學生2的學號,學生1的學號也發生了變化呢?
原因出在(stu2 = stu1) 這一句。該語句的作用是將stu1的引用賦值給stu2,
這樣,stu1和stu2指向內存堆中同一個對象。如圖:

那么,怎樣才能達到復制一個對象呢?
是否記得萬類之王Object。它有11個方法,有兩個protected的方法,其中一個為clone方法。
在Java中所有的類都是缺省的繼承自Java語言包中的Object類的,查看它的源碼,你可以把你的JDK目錄下的src.zip復制到其他地方然后解壓,里面就是所有的源碼。發現里面有一個訪問限定符為protected的方法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 x, the expression:
1) x.clone() != x will be true
2) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
3) x.clone().equals(x) will be true, this is not an absolute requirement.
*/
protected native Object clone() throws CloneNotSupportedException;
~~~

仔細一看,它還是一個native方法,大家都知道native方法是非Java語言實現的代碼,供Java程序調用的,因為Java程序是運行在JVM虛擬機上面的,要想訪問到比較底層的與操作系統相關的就沒辦法了,只能由靠近操作系統的語言來實現。
1. 第一次聲明保證克隆對象將有單獨的內存地址分配。
2. 第二次聲明表明,原始和克隆的對象應該具有相同的類類型,但它不是強制性的。
3. 第三聲明表明,原始和克隆的對象應該是平等的equals()方法使用,但它不是強制性的。
因為每個類直接或間接的父類都是Object,因此它們都含有clone()方法,但是因為該方法是protected,所以都不能在類外進行訪問。
要想對一個對象進行復制,就需要對clone方法覆蓋。
[回到頂部](http://www.cnblogs.com/Qian123/p/5710533.html#_labelTop)
## 為什么要克隆?
大家先思考一個問題,為什么需要克隆對象?直接new一個對象不行嗎?
答案是:克隆的對象可能包含一些已經修改過的屬性,而new出來的對象的屬性都還是初始化時候的值,所以當需要一個新的對象來保存當前對象的“狀態”就靠clone方法了。那么我把這個對象的臨時屬性一個一個的賦值給我新new的對象不也行嘛?可以是可以,但是一來麻煩不說,二來,大家通過上面的源碼都發現了clone是一個native方法,就是快啊,在底層實現的。
提個醒,我們常見的Object a=new Object();Object b;b=a;這種形式的代碼復制的是引用,即對象在內存中的地址,a和b對象仍然指向了同一個對象。
而通過clone方法賦值的對象跟原來的對象時同時獨立存在的。
[回到頂部](http://www.cnblogs.com/Qian123/p/5710533.html#_labelTop)
## 如何實現克隆
先介紹一下兩種不同的克隆方法,**淺克隆(ShallowClone)**和**深克隆(DeepClone)**。
在Java語言中,數據類型分為值類型(基本數據類型)和引用類型,值類型包括int、double、byte、boolean、char等簡單數據類型,引用類型包括類、接口、數組等復雜類型。淺克隆和深克隆的主要區別在于是否支持引用類型的成員變量的復制,下面將對兩者進行詳細介紹。
一般步驟是(淺克隆):
1.?**被復制的類需要實現Clonenable接口**(不實現的話在調用clone方法會拋出CloneNotSupportedException異常), 該接口為標記接口(不含任何方法)
2.?**覆蓋clone()方法,訪問修飾符設為public**。**方法中調用super.clone()方法得到需要的復制對象**。(native為本地方法)
下面對上面那個方法進行改造:

~~~
class Student implements Cloneable{
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = (Student)stu1.clone();
System.out.println("學生1:" + stu1.getNumber());
System.out.println("學生2:" + stu2.getNumber());
stu2.setNumber(54321);
System.out.println("學生1:" + stu1.getNumber());
System.out.println("學生2:" + stu2.getNumber());
}
}
~~~

結果:
學生1:12345??
學生2:12345??
學生1:12345??
學生2:54321
如果你還不相信這兩個對象不是同一個對象,那么你可以看看這一句:
~~~
System.out.println(stu1 == stu2); // false
~~~
上面的復制被稱為淺克隆。
還有一種稍微復雜的深度復制:
我們在學生類里再加一個Address類。

~~~
1 class Address {
2 private String add;
3
4 public String getAdd() {
5 return add;
6 }
7
8 public void setAdd(String add) {
9 this.add = add;
10 }
11
12 }
13
14 class Student implements Cloneable{
15 private int number;
16
17 private Address addr;
18
19 public Address getAddr() {
20 return addr;
21 }
22
23 public void setAddr(Address addr) {
24 this.addr = addr;
25 }
26
27 public int getNumber() {
28 return number;
29 }
30
31 public void setNumber(int number) {
32 this.number = number;
33 }
34
35 @Override
36 public Object clone() {
37 Student stu = null;
38 try{
39 stu = (Student)super.clone();
40 }catch(CloneNotSupportedException e) {
41 e.printStackTrace();
42 }
43 return stu;
44 }
45 }
46 public class Test {
47
48 public static void main(String args[]) {
49
50 Address addr = new Address();
51 addr.setAdd("杭州市");
52 Student stu1 = new Student();
53 stu1.setNumber(123);
54 stu1.setAddr(addr);
55
56 Student stu2 = (Student)stu1.clone();
57
58 System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
59 System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
60 }
61 }
~~~

結果:
學生1:123,地址:杭州市??
學生2:123,地址:杭州市 ?
乍一看沒什么問題,真的是這樣嗎?
我們在main方法中試著改變addr實例的地址。
~~~
addr.setAdd("西湖區");
System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
~~~
結果:
~~~
學生1:123,地址:杭州市
學生2:123,地址:杭州市
學生1:123,地址:西湖區
學生2:123,地址:西湖區
~~~
這就奇怪了,怎么兩個學生的地址都改變了?
原因是淺復制只是復制了addr變量的引用,并沒有真正的開辟另一塊空間,將值復制后再將引用返回給新對象。
所以,為了達到真正的復制對象,而不是純粹引用復制。我們需要將Address類可復制化,并且修改clone方法,完整代碼如下:

~~~
1 package abc;
2
3 class Address implements Cloneable {
4 private String add;
5
6 public String getAdd() {
7 return add;
8 }
9
10 public void setAdd(String add) {
11 this.add = add;
12 }
13
14 @Override
15 public Object clone() {
16 Address addr = null;
17 try{
18 addr = (Address)super.clone();
19 }catch(CloneNotSupportedException e) {
20 e.printStackTrace();
21 }
22 return addr;
23 }
24 }
25
26 class Student implements Cloneable{
27 private int number;
28
29 private Address addr;
30
31 public Address getAddr() {
32 return addr;
33 }
34
35 public void setAddr(Address addr) {
36 this.addr = addr;
37 }
38
39 public int getNumber() {
40 return number;
41 }
42
43 public void setNumber(int number) {
44 this.number = number;
45 }
46
47 @Override
48 public Object clone() {
49 Student stu = null;
50 try{
51 stu = (Student)super.clone(); //淺復制
52 }catch(CloneNotSupportedException e) {
53 e.printStackTrace();
54 }
55 stu.addr = (Address)addr.clone(); //深度復制
56 return stu;
57 }
58 }
59 public class Test {
60
61 public static void main(String args[]) {
62
63 Address addr = new Address();
64 addr.setAdd("杭州市");
65 Student stu1 = new Student();
66 stu1.setNumber(123);
67 stu1.setAddr(addr);
68
69 Student stu2 = (Student)stu1.clone();
70
71 System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
72 System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
73
74 addr.setAdd("西湖區");
75
76 System.out.println("學生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());
77 System.out.println("學生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());
78 }
79 }
~~~

結果:
~~~
學生1:123,地址:杭州市
學生2:123,地址:杭州市
學生1:123,地址:西湖區
學生2:123,地址:杭州市
~~~
這樣結果就符合我們的想法了。
最后我們可以看看API里其中一個實現了clone方法的類:
java.util.Date:

~~~
/**
* Return a copy of this object.
*/
public Object clone() {
Date d = null;
try {
d = (Date)super.clone();
if (cdate != null) {
d.cdate = (BaseCalendar.Date) cdate.clone();
}
} catch (CloneNotSupportedException e) {} // Won't happen
return d;
}
~~~

該類其實也屬于深度復制。
參考文檔:[Java如何復制對象](http://blog.csdn.net/tounaobun/article/details/8491392)
[回到頂部](http://www.cnblogs.com/Qian123/p/5710533.html#_labelTop)
## 淺克隆和深克隆
### 1、淺克隆
在淺克隆中,如果原型對象的成員變量是值類型,將復制一份給克隆對象;如果原型對象的成員變量是引用類型,則將引用對象的地址復制一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的內存地址。
簡單來說,在淺克隆中,當對象被復制時只復制它本身和其中包含的值類型的成員變量,而引用類型的成員對象并沒有復制。

在Java語言中,通過**覆蓋Object類的clone()方法可以實現淺克隆**。
### 2、深克隆
在深克隆中,無論原型對象的成員變量是值類型還是引用類型,都將復制一份給克隆對象,深克隆將原型對象的所有引用對象也復制一份給克隆對象。
簡單來說,在深克隆中,除了對象本身被復制外,對象所包含的所有成員變量也將復制。

在Java語言中,如果需要**實現深克隆,可以通過**覆蓋Object類的clone()方法實現,也**可以通過序列化(Serialization)等方式**來實現。
(**如果引用類型里面還包含很多引用類型,或者內層引用類型的類里面又包含引用類型,使用clone方法就會很麻煩。這時我們可以用序列化的方式來實現對象的深克隆。**)
序列化就是將對象寫到流的過程,寫到流中的對象是原有對象的一個拷貝,而原對象仍然存在于內存中。通過序列化實現的拷貝不僅可以復制對象本身,而且可以復制其引用的成員對象,因此通過序列化將對象寫到一個流中,再從流里將其讀出來,可以實現深克隆。需要注意的是能夠實現序列化的對象其類必須實現Serializable接口,否則無法實現序列化操作。
擴展
| Java語言提供的Cloneable接口和Serializable接口的代碼非常簡單,它們都是空接口,這種空接口也稱為標識接口,標識接口中沒有任何方法的定義,其作用是告訴JRE這些接口的實現類是否具有某個功能,如是否支持克隆、是否支持序列化等。 |
[回到頂部](http://www.cnblogs.com/Qian123/p/5710533.html#_labelTop)
## 解決多層克隆問題
如果引用類型里面還包含很多引用類型,或者內層引用類型的類里面又包含引用類型,使用clone方法就會很麻煩。這時我們可以用序列化的方式來實現對象的深克隆。

~~~
1 public class Outer implements Serializable{
2 private static final long serialVersionUID = 369285298572941L; //最好是顯式聲明ID
3 public Inner inner;
4 //Discription:[深度復制方法,需要對象及對象所有的對象屬性都實現序列化]
5 public Outer myclone() {
6 Outer outer = null;
7 try {?// 將該對象序列化成流,因為寫在流里的是對象的一個拷貝,而原對象仍然存在于JVM里面。所以利用這個特性可以實現對象的深拷貝
8 ByteArrayOutputStream baos = new ByteArrayOutputStream();
9 ObjectOutputStream oos = new ObjectOutputStream(baos);
10 oos.writeObject(this);
11 // 將流序列化成對象
12 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
13 ObjectInputStream ois = new ObjectInputStream(bais);
14 outer = (Outer) ois.readObject();
15 } catch (IOException e) {
16 e.printStackTrace();
17 } catch (ClassNotFoundException e) {
18 e.printStackTrace();
19 }
20 return outer;
21 }
22 }
~~~

Inner也必須實現Serializable,否則無法序列化:

~~~
1 public class Inner implements Serializable{
2 private static final long serialVersionUID = 872390113109L; //最好是顯式聲明ID
3 public String name = "";
4
5 public Inner(String name) {
6 this.name = name;
7 }
8
9 @Override
10 public String toString() {
11 return "Inner的name值為:" + name;
12 }
13 }
~~~

這樣也能使兩個對象在內存空間內完全獨立存在,互不影響對方的值。
[回到頂部](http://www.cnblogs.com/Qian123/p/5710533.html#_labelTop)
## 總結
實現對象克隆有兩種方式:
??1). 實現Cloneable接口并重寫Object類中的clone()方法;
??2). 實現Serializable接口,通過對象的序列化和反序列化實現克隆,可以實現真正的深度克隆。
|
**注意:**基于序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是通過泛型限定,可以檢查出要克隆的對象是否支持序列化,這項檢查是編譯器完成的,不是在運行時拋出異常,這種是方案明顯優于使用Object類的clone方法克隆對象。讓問題在編譯的時候暴露出來總是優于把問題留到運行時。
|
文章鏈接:
[Java進階系列之對象克隆](http://www.jianshu.com/p/36dfa5457b3e)
[Java基礎筆記 – 對象的深復制與淺復制 實現Cloneable接口實現淺復制 序列化實現深復制](http://www.itzhai.com/java-based-notebook-the-object-of-deep-and-shallow-copy-copy-copy-implement-the-cloneable-interface-serializing-deep-deep-copy.html#read-more)
[詳解Java中的對象克隆](http://www.jianshu.com/p/c31e01a5d55d)
[Java對象克隆指南](https://yq.aliyun.com/articles/44723)
[對象的克隆——原型模式(三)](https://quanke.gitbooks.io/design-pattern-java/content/%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%85%8B%E9%9A%86%E2%80%94%E2%80%94%E5%8E%9F%E5%9E%8B%E6%A8%A1%E5%BC%8F%EF%BC%88%E4%B8%89%EF%BC%89.html)
[java提高篇(五)-----使用序列化實現對象的拷貝](http://blog.csdn.net/chenssy/article/details/12952063)
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)