1. 程式人生 > >Java提高篇——物件克隆(複製)

Java提高篇——物件克隆(複製)

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

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方法覆蓋。

為什麼要克隆?

  大家先思考一個問題,為什麼需要克隆物件?直接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;  
}  

該類其實也屬於深度複製。

淺克隆和深克隆

1、淺克隆

在淺克隆中,如果原型物件的成員變數是值型別,將複製一份給克隆物件;如果原型物件的成員變數是引用型別,則將引用物件的地址複製一份給克隆物件,也就是說原型物件和克隆物件的成員變數指向相同的記憶體地址。

簡單來說,在淺克隆中,當物件被複制時只複製它本身和其中包含的值型別的成員變數,而引用型別的成員物件並沒有複製。

在Java語言中,通過覆蓋Object類的clone()方法可以實現淺克隆

2、深克隆

在深克隆中,無論原型物件的成員變數是值型別還是引用型別,都將複製一份給克隆物件,深克隆將原型物件的所有引用物件也複製一份給克隆物件。

簡單來說,在深克隆中,除了物件本身被複制外,物件所包含的所有成員變數也將複製。

在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 {// 將該物件序列化成流,因為寫在流裡的是物件的一個拷貝,而原物件仍然存在於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 }

這樣也能使兩個物件在記憶體空間內完全獨立存在,互不影響對方的值。

總結

實現物件克隆有兩種方式:

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

  2). 實現Serializable介面,通過物件的序列化和反序列化實現克隆,可以實現真正的深度克隆。

注意:基於序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是通過泛型限定,可以檢查出要克隆的物件是否支援序列化,這項檢查是編譯器完成的,不是在執行時丟擲異常,這種是方案明顯優於使用Object類的clone方法克隆物件。讓問題在編譯的時候暴露出來總是優於把問題留到執行時。

文章連結:

相關推薦

Java提高——物件克隆複製

假如說你想複製一個簡單變數。很簡單: int apples = 5; int pears = apples; 不僅僅是int型別,其它七種原始資料型別(boolean,char,byte,short,float,double.long)同樣適用於該類情況。 但是如果你複製的是一個物件,

Java基礎4物件克隆複製

以下介紹兩種不同的克隆方法,淺克隆(ShallowClone)和深克隆(DeepClone)。在Java語言中,資料型別分為值型別(基本資料型別)和引用型別,值型別包括int、double、byte、boolean、char等簡單資料型別,引用型別包括類、介面、陣列等複雜型別

Maven 實戰 -多模組 vs 繼承 Maven提高系列之——多模組 vs 繼承

Maven提高篇系列之(一)——多模組 vs 繼承     這是一個Maven提高篇的系列,包含有以下文章:    Maven提高篇系列之(一)——多模組 vs 繼承 Maven提高篇系列之(二)——配置Plu

Java基礎——常用物件API——集合

一、集合框架 1.概述 面向物件的語言會產生很多物件,為了方便儲存,就要有容器來儲存這些物件。區別於陣列,集合是可變長度的。 注意:集合中不可以儲存基本資料型別。 簡單總結一下,集合與陣列的區別在於:長度可變、不能儲存基本資料型別。 2.體系&共性功能

Java基礎——常用物件API——基本資料型別物件包裝類

一、概述 為了方便操作基本資料型別值,將其封裝成了物件,在物件中定義了屬性和行為豐富了該資料的操作。用於描述該物件的類就成為基本資料型別物件包裝類。 物件 類名 byte Byte short

Java基礎——常用物件APIString類和StringBuffer類

String 一、特點 字串是一個特殊的物件。 字串一旦初始化就不可被改變。 問:String str = "abc" 和 String str1 = new String("abc") 有什麼區別? new出來的物件在堆記憶體中,產生兩個物件但都在堆記

Java基礎——常用物件APIMap集合

一、集合特點和常用方法 1.特點 一次新增一對元素,因此也稱作雙列集合。(相較下,Set稱為單列集合) 要保證鍵的唯一性。 2.功能 2.1.新增 value put(key,value);//返回前一個和key關聯的值,如果沒有返回null。 2.2.刪除 vo

Java實現面向物件程式設計入門

一、◆抽象和封裝1、現實世界是“面向物件”的,面向物件就是採用“現實模擬”的方法設計和開發程式。從現實中抽象出類:①發現類 ②發現類的屬性 ③發現類的方法用面向物件的思想描述面向物件的世界,符合人類的思維習慣。(類圖用於分析和設計類,更直觀、容易理解。)2、面向物件設計的過

Maven提高系列之——多模組 vs 繼承

通常來說,在Maven的多模組工程中,都存在一個pom型別的工程作為根模組,該工程只包含一個pom.xml檔案,在該檔案中以模組(module)的形式宣告它所包含的子模組,即多模組工程。在子模組的pom.xml檔案中,又以parent的形式宣告其所屬的父模組,即繼承

Java提高——多執行緒join、sleep、yield

join、sleep、yield都是Thread類的方法join執行緒join()方法:讓“主執行緒”執行緒等待“子執行緒”執行完之後再執行。//子執行緒 public class son extends Thread(){ void run(){

Java提高——多執行緒生產消費者問題

生產者/消費者問題是個典型的多執行緒問題,類似於hello world對於一門程式語言而言,涉及的物件包括“生產者”、“消費者”、“倉庫”和“產品”。該模型需要注意以下幾點:1、生產者只有在倉庫未滿的時候生產,倉滿則停止生產。2、消費者只有在倉庫有產品的情況下才能消費,空倉則

Java IO流中拷貝複製檔案的寫法

1. 使用位元組流讀寫複製檔案/**     * 位元組流讀寫複製檔案     * @param src 原始檔     * @param out 目標檔案     */    public static void InputStreamOutputStream(String

Java提高:區分引用變數與物件

我們有程式碼: New A=new New(); 下面是這個New的類: class New { public New() { System.out.println

Java入門提高】Day5 Java中的回調

彈出對話框 java入門 也會 color 編程 args performed show clas   Java中有很多個Timer,常用的有兩個Timer類,一個java.util包下的Timer,一個是javax.swing包下的Timer,兩個Timer類都有用到回調

java提高——基礎查缺補漏面試

連結:http://blog.csdn.net/xiaokang123456kao/article/details/54233595 連結失效請複製改連結至位址列 一.java基本資料型別所佔的記憶體大小 在Java中一共有8種基本資料型別,其中有4種整型,2種浮點型別,1種用於表示Uni

物件克隆C# 快速高效率複製物件另一種方式 表示式樹轉

1、需求 在程式碼中經常會遇到需要把物件複製一遍,或者把屬性名相同的值複製一遍。 比如:   public class Student { public int Id { get; set; } public string N

Java提高:內部類和匿名內部類

1 public class innerclass { 2 public static void main(String[] args) { 3 System.out.println("下面是是內部類的程式展示"); 4 //建立外部類和內部類的方法有點不相同

java提高-----實現多重繼承

       多重繼承指的是一個類可以同時從多於一個的父類那裡繼承行為和特徵,然而我們知道Java為了保證資料安全,它只允許單繼承。有些時候我們會認為如果系統中需要使用多重繼承往往都是糟糕的設計,這個

java提高-----詳解匿名內部類

       在java提高篇-----詳解內部類中對匿名內部類做了一個簡單的介紹,但是內部類還存在很多其他細節問題,所以就衍生出這篇部落格。在這篇部落格中你可以瞭解到匿名內部類的使用、匿名內部類要注

java提高-----詳解內部類

      可以將一個類的定義放在另一個類的定義內部,這就是內部類。        內部類是一個非常有用的特性但又比較難理解使用的特性(鄙人到現在都沒有怎麼使用過內部類,對內部類也只是略知一二)。