天天看點

Java的深拷貝和淺拷貝

  關于Java的深拷貝和淺拷貝,簡單來說就是建立一個和已知對象一模一樣的對象。可能日常編碼過程中用的不多,但是這是一個面試經常會問的問題,而且了解深拷貝和淺拷貝的原理,對于Java中的所謂值傳遞或者引用傳遞将會有更深的了解。

1、建立對象的5種方式

  ①、通過 new 關鍵字

  這是最常用的一種方式,通過 new 關鍵字調用類的有參或無參構造方法來建立對象。比如 Object obj = new Object();

  ②、通過 Class 類的 newInstance() 方法

  這種預設是調用類的無參構造方法建立對象。比如 Person p2 = (Person) Class.forName("com.ys.test.Person").newInstance();

  ③、通過 Constructor 類的 newInstance 方法

  這和第二種方法類時,都是通過反射來實作。通過 java.lang.relect.Constructor 類的 newInstance() 方法指定某個構造器來建立對象。

  Person p3 = (Person) Person.class.getConstructors()[0].newInstance();

  實際上第二種方法利用 Class 的 newInstance() 方法建立對象,其内部調用還是 Constructor 的 newInstance() 方法。

  ④、利用 Clone 方法

  Clone 是 Object 類中的一個方法,通過 對象A.clone() 方法會建立一個内容和對象 A 一模一樣的對象 B,clone 克隆,顧名思義就是建立一個一模一樣的對象出來。

  Person p4 = (Person) p3.clone();

  ⑤、反序列化

  序列化是把堆記憶體中的 Java 對象資料,通過某種方式把對象存儲到磁盤檔案中或者傳遞給其他網絡節點(在網絡上傳輸)。而反序列化則是把磁盤檔案中的對象資料或者把網絡節點上的對象資料,恢複成Java對象模型的過程。

  具體如何實作可以參考我 

這篇博文

3、Clone 方法

  本篇部落格我們講解的是 Java 的深拷貝和淺拷貝,其實作方式正是通過調用 Object 類的 clone() 方法來完成。在 Object.class 類中,源碼為:

protected native Object clone() throws CloneNotSupportedException;      

  這是一個用 native 關鍵字修飾的方法,關于native關鍵字有一篇

部落格

專門有介紹,不了解也沒關系,隻需要知道用 native 修飾的方法就是告訴作業系統,這個方法我不實作了,讓作業系統去實作。具體怎麼實作我們不需要了解,隻需要知道 clone方法的作用就是複制對象,産生一個新的對象。那麼這個新的對象和原對象是什麼關系呢?

4、基本類型和引用類型

  這裡再給大家普及一個概念,在 Java 中基本類型和引用類型的差別。

  在 Java 中資料類型可以分為兩大類:基本類型和引用類型。

  基本類型也稱為值類型,分别是字元類型 char,布爾類型 boolean以及數值類型 byte、short、int、long、float、double。

  引用類型則包括類、接口、數組、枚舉等。

  Java 将記憶體空間分為堆和棧。基本類型直接在棧中存儲數值,而引用類型是将引用放在棧中,實際存儲的值是放在堆中,通過棧中的引用指向堆中存放的資料。

  

Java的深拷貝和淺拷貝

  上圖定義的 a 和 b 都是基本類型,其值是直接存放在棧中的;而 c 和 d 是 String 聲明的,這是一個引用類型,引用位址是存放在 棧中,然後指向堆的記憶體空間。

  下面 d = c;這條語句表示将 c 的引用指派給 d,那麼 c 和 d 将指向同一塊堆記憶體空間。

5、淺拷貝

  我們看如下這段代碼:

Java的深拷貝和淺拷貝
Java的深拷貝和淺拷貝
package com.ys.test;

public class Person implements Cloneable{
    public String pname;
    public int page;
    public Address address;
    public Person() {}
    
    public Person(String pname,int page){
        this.pname = pname;
        this.page = page;
        this.address = new Address();
    }
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    public void setAddress(String provices,String city ){
        address.setAddress(provices, city);
    }
    public void display(String name){
        System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+ address);
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }
    
}      

View Code

Java的深拷貝和淺拷貝
Java的深拷貝和淺拷貝
package com.ys.test;

public class Address {
    private String provices;
    private String city;
    public void setAddress(String provices,String city){
        this.provices = provices;
        this.city = city;
    }
    @Override
    public String toString() {
        return "Address [provices=" + provices + ", city=" + city + "]";
    }
    
}      

  這是一個我們要進行指派的原始類 Person。下面我們産生一個 Person 對象,并調用其 clone 方法複制一個新的對象。

  注意:調用對象的 clone 方法,必須要讓類實作 Cloneable 接口,并且覆寫 clone 方法。

  測試:

Java的深拷貝和淺拷貝
Java的深拷貝和淺拷貝
@Test
public void testShallowClone() throws Exception{
    Person p1 = new Person("zhangsan",21);
    p1.setAddress("湖北省", "武漢市");
    Person p2 = (Person) p1.clone();
    System.out.println("p1:"+p1);
    System.out.println("p1.getPname:"+p1.getPname().hashCode());
    
    System.out.println("p2:"+p2);
    System.out.println("p2.getPname:"+p2.getPname().hashCode());
    
    p1.display("p1");
    p2.display("p2");
    p2.setAddress("湖北省", "荊州市");
    System.out.println("将複制之後的對象位址修改:");
    p1.display("p1");
    p2.display("p2");
}      

  列印結果為:

Java的深拷貝和淺拷貝

  首先看原始類 Person 實作 Cloneable 接口,并且覆寫 clone 方法,它還有三個屬性,一個引用類型 String定義的 pname,一個基本類型 int定義的 page,還有一個引用類型 Address ,這是一個自定義類,這個類也包含兩個屬性 pprovices 和 city 。

  接着看測試内容,首先我們建立一個Person 類的對象 p1,其pname 為zhangsan,page為21,位址類 Address 兩個屬性為 湖北省和武漢市。接着我們調用 clone() 方法複制另一個對象 p2,接着列印這兩個對象的内容。

  從第 1 行和第 3 行列印結果:

  p1:com.ys.test.Person@349319f9

  p2:com.ys.test.Person@258e4566

  可以看出這是兩個不同的對象。

  從第 5 行和第 6 行列印的對象内容看,原對象 p1 和克隆出來的對象 p2 内容完全相同。

  代碼中我們隻是更改了克隆對象 p2 的屬性 Address 為湖北省荊州市(原對象 p1 是湖北省武漢市) ,但是從第 7 行和第 8 行列印結果來看,原對象 p1 和克隆對象 p2 的 Address 屬性都被修改了。

  也就是說對象 Person 的屬性 Address,經過 clone 之後,其實隻是複制了其引用,他們指向的還是同一塊堆記憶體空間,當修改其中一個對象的屬性 Address,另一個也會跟着變化。

Java的深拷貝和淺拷貝

  淺拷貝:建立一個新對象,然後将目前對象的非靜态字段複制到該新對象,如果字段是值類型的,那麼對該字段執行複制;如果該字段是引用類型的話,則複制引用但不複制引用的對象。是以,原始對象及其副本引用同一個對象。

6、深拷貝

  弄清楚了淺拷貝,那麼深拷貝就很容易了解了。

  深拷貝:建立一個新對象,然後将目前對象的非靜态字段複制到該新對象,無論該字段是值類型的還是引用類型,都複制獨立的一份。當你修改其中一個對象的任何内容時,都不會影響另一個對象的内容。

Java的深拷貝和淺拷貝

  那麼該如何實作深拷貝呢?Object 類提供的 clone 是隻能實作 淺拷貝的。

7、如何實作深拷貝?

  深拷貝的原理我們知道了,就是要讓原始對象和克隆之後的對象所具有的引用類型屬性不是指向同一塊堆記憶體,這裡有三種實作思路。

  ①、讓每個引用類型屬性内部都重寫clone() 方法

  既然引用類型不能實作深拷貝,那麼我們将每個引用類型都拆分為基本類型,分别進行淺拷貝。比如上面的例子,Person 類有一個引用類型 Address(其實String 也是引用類型,但是String類型有點特殊,後面會詳細講解),我們在 Address 類内部也重寫 clone 方法。如下:

  Address.class:

Java的深拷貝和淺拷貝
Java的深拷貝和淺拷貝
1 package com.ys.test;
 2 
 3 public class Address implements Cloneable{
 4     private String provices;
 5     private String city;
 6     public void setAddress(String provices,String city){
 7         this.provices = provices;
 8         this.city = city;
 9     }
10     @Override
11     public String toString() {
12         return "Address [provices=" + provices + ", city=" + city + "]";
13     }
14     @Override
15     protected Object clone() throws CloneNotSupportedException {
16         return super.clone();
17     }
18     
19 }      

  Person.class 的 clone() 方法:

Java的深拷貝和淺拷貝
Java的深拷貝和淺拷貝
1     @Override
2     protected Object clone() throws CloneNotSupportedException {
3         Person p = (Person) super.clone();
4         p.address = (Address) address.clone();
5         return p;
6     }      

  測試還是和上面一樣,我們會發現更改了p2對象的Address屬性,p1 對象的 Address 屬性并沒有變化。

  但是這種做法有個弊端,這裡我們Person 類隻有一個 Address 引用類型,而 Address 類沒有,是以我們隻用重寫 Address 類的clone 方法,但是如果 Address 類也存在一個引用類型,那麼我們也要重寫其clone 方法,這樣下去,有多少個引用類型,我們就要重寫多少次,如果存在很多引用類型,那麼代碼量顯然會很大,是以這種方法不太合适。

  ②、利用序列化

  序列化是将對象寫到流中便于傳輸,而反序列化則是把對象從流中讀取出來。這裡寫到流中的對象則是原始對象的一個拷貝,因為原始對象還存在 JVM 中,是以我們可以利用對象的序列化産生克隆對象,然後通過反序列化擷取這個對象。

  注意每個需要序列化的類都要實作 Serializable 接口,如果有某個屬性不需要序列化,可以将其聲明為 transient,即将其排除在克隆屬性之外。

Java的深拷貝和淺拷貝
Java的深拷貝和淺拷貝
//深度拷貝
public Object deepClone() throws Exception{
    // 序列化
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bos);

    oos.writeObject(this);

    // 反序列化
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bis);

    return ois.readObject();
}      

   因為序列化産生的是兩個完全獨立的對象,所有無論嵌套多少個引用類型,序列化都是能實作深拷貝的。

作者:

YSOcean

出處:

http://www.cnblogs.com/ysocean/

本文版權歸作者所有,歡迎轉載,但未經作者同意不能轉載,否則保留追究法律責任的權利。