Java 對象克隆(複製)

閱讀目錄html

假如說你想複製一個簡單變量。很簡單:java

int apples = 5;  
int pears = apples;

不只僅是int類型,其它七種原始數據類型(boolean,char,byte,short,float,double.long)一樣適用於該類狀況。express

可是若是你複製的是一個對象,狀況就有些複雜了。數組

假設說我是一個beginner,我會這樣寫:app

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());  
    }  
}

結果:ide

學生1:12345  ui

學生2:12345  this

 

這裏咱們自定義了一個學生類,該類只有一個number字段。spa

咱們新建了一個學生實例,而後將該值賦值給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方法覆蓋。

 

爲何要克隆?

  你們先思考一個問題,爲何須要克隆對象?直接new一個對象不行嗎?

  答案是:克隆的對象可能包含一些已經修改過的屬性,而new出來的對象的屬性都仍是初始化時候的值,因此當須要一個新的對象來保存當前對象的「狀態」就靠clone方法了。那麼我把這個對象的臨時屬性一個一個的賦值給我新new的對象不也行嘛?能夠是能夠,可是一來麻煩不說,二來,你們經過上面的源碼都發現了clone是一個native方法,就是快啊,在底層實現的。

  提個醒,咱們常見的Object a=new Object();Object b;b=a;這種形式的代碼複製的是引用,即對象在內存中的地址,a和b對象仍然指向了同一個對象。

  而經過clone方法賦值的對象跟原來的對象時同時獨立存在的。

 

如何實現克隆

先介紹一下兩種不一樣的克隆方法,淺克隆(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如何複製對象

 

淺克隆和深克隆

一、淺克隆

在淺克隆中,若是原型對象的成員變量是值類型,將複製一份給克隆對象;若是原型對象的成員變量是引用類型,則將引用對象的地址複製一份給克隆對象,也就是說原型對象和克隆對象的成員變量指向相同的內存地址。

簡單來講,在淺克隆中,當對象被複制時只複製它自己和其中包含的值類型的成員變量,而引用類型的成員對象並無複製。

在Java語言中,經過覆蓋Object類的clone()方法能夠實現淺克隆

二、深克隆

在深克隆中,不管原型對象的成員變量是值類型仍是引用類型,都將複製一份給克隆對象,深克隆將原型對象的全部引用對象也複製一份給克隆對象。

簡單來講,在深克隆中,除了對象自己被複制外,對象所包含的全部成員變量也將複製。

在Java語言中,若是須要實現深克隆,能夠經過覆蓋Object類的clone()方法實現,也能夠經過序列化(Serialization)等方式來實現。

若是引用類型裏面還包含不少引用類型,或者內層引用類型的類裏面又包含引用類型,使用clone方法就會很麻煩。這時咱們能夠用序列化的方式來實現對象的深克隆。

序列化就是將對象寫到流的過程,寫到流中的對象是原有對象的一個拷貝,而原對象仍然存在於內存中。經過序列化實現的拷貝不只能夠複製對象自己,並且能夠複製其引用的成員對象,所以經過序列化將對象寫到一個流中,再從流裏將其讀出來,能夠實現深克隆。須要注意的是可以實現序列化的對象其類必須實現Serializable接口,不然沒法實現序列化操做。

擴展
Java語言提供的Cloneable接口和Serializable接口的代碼很是簡單,它們都是空接口,這種空接口也稱爲標識接口,標識接口中沒有任何方法的定義,其做用是告訴JRE這些接口的實現類是否具備某個功能,如是否支持克隆、是否支持序列化等。

 

解決多層克隆問題

若是引用類型裏面還包含不少引用類型,或者內層引用類型的類裏面又包含引用類型,使用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 {
 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 } // 將該對象序列化成流,由於寫在流裏的是對象的一個拷貝,而原對象仍然存在於JVM裏面。因此利用這個特性能夠實現對象的深拷貝

 

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 }

 

這樣也能使兩個對象在內存空間內徹底獨立存在,互不影響對方的值。

 

總結

實現對象克隆有兩種方式:

  1). 實現Cloneable接口並重寫Object類中的clone()方法;

  2). 實現Serializable接口,經過對象的序列化和反序列化實現克隆,能夠實現真正的深度克隆。

注意:基於序列化和反序列化實現的克隆不只僅是深度克隆,更重要的是經過泛型限定,能夠檢查出要克隆的對象是否支持序列化,這項檢查是編譯器完成的,不是在運行時拋出異常,這種是方案明顯優於使用Object類的clone方法克隆對象。讓問題在編譯的時候暴露出來老是優於把問題留到運行時。

文章連接:

相關文章
相關標籤/搜索