Java之deep copy(深復制)


前段時間碰到需要將一個Java對象進行深度拷貝的情況,但是JDK並未提供關於deep copy相關的API,唯一能用的就是一個不太穩定的clone(),所以問題就來了,如何實現穩定的deep copy,下面就實現deep copy的方法做個介紹。

1. 直接賦值

實現deep copy,首先想到的是可以直接賦值么?如下:

 

  1.  
    Test test = new Test();
  2.  
    Test test2 = test;
  3.  
     
  4.  
    System.out.println(test);
  5.  
    System.out.println(test2);

上面的代碼里,直接將test復制給test2,但是將兩個對象打印出來發現,地址其實是一樣的,test只是剛剛在堆上分配的Test對象的引用,而這里的賦值直接是引用直接的賦值,等於test2也是指向剛剛new出來的對象,這里的copy就是一個shallow copy,及只是copy了一份引用,但是對象實體並未copy,既然賦值不行,那就試試第二個方法,Object類的clone方法。

2. clone方法

1. clone方法介紹

Java中所有對象都繼承自Object類,所以就默認自帶clone方法的實現,clone方法的實現是比較簡單粗暴的。首先,如果一個對象想要調用clone方法,必須實現Cloneable接口,否則會拋出CloneNotSupportedException。其實這個Cloneable是個空接口,只是個flag用來標記這個類是可以clone的,所以說將一個類聲明為Cloneable與這個類具備clone能力其實並不是直接相關的。其實Cloneable是想表明具有復制這種功能,所以按理說clone應該作為Cloneable的一個方法而存在,但是實際上clone方法是Object類的一個protected方法,所以你無法直接通過多態的方式調用clone方法,比如:

 

  1.  
    public class Test implements Cloneable {
  2.  
     
  3.  
    public static void main(String[] args) {
  4.  
    try {
  5.  
    List<Cloneable> list = new ArrayList<Cloneable>();
  6.  
    Cloneable t1 = new InnerTest("test");
  7.  
    list.add(t1);
  8.  
    list.add(t1.clone()); // 事實上,我無法這么做
  9.  
    } catch (Exception e) {
  10.  
    e.printStackTrace();
  11.  
    }
  12.  
    }
  13.  
     
  14.  
    public static class InnerTest implements Cloneable {
  15.  
    public String a;
  16.  
     
  17.  
    public InnerTest(String test) {
  18.  
    a = test;
  19.  
    }
  20.  
    public Object clone() throws CloneNotSupportedException {
  21.  
    return super.clone();
  22.  
    }
  23.  
    }
  24.  
    }

這其實是設計上的一個缺陷,不過導致clone方法聲名狼藉的並不單單因為這個。

 

2. clone是深復制還是淺復制

當調用clone方法時,首先會直接分配內存,然后將原對象內所有的字段都一一復制,如果字段是基本類型數據比如int之類的,則這樣直接的賦值式的復制毫無問題,但是如果字段是引用的話問題就來了,引用也會原封不動的復制一份,就如同第一個例子一樣。所以,很多情景下,clone只能算一個半deep半shallow的復制方法。想要解決這個問題,唯一的方法就是在需要被復制的對象的clone方法內調用會被shallow copy的對象的clone方法,但是前提是該對象也繼承了Cloneable接口並Override了clone方法。比如:

 

  1.  
    public class Test implements Cloneable {
  2.  
     
  3.  
    public static void main(String[] args) {
  4.  
    try {
  5.  
    InnerTest t1 = new InnerTest(new InnerTest2());
  6.  
    InnerTest t2 = (InnerTest) t1.clone();
  7.  
    System.out.println(t1); // Test$InnerTest@232204a1
  8.  
    System.out.println(t2); // Test$InnerTest@4aa298b7
  9.  
    } catch (Exception e) {
  10.  
    e.printStackTrace();
  11.  
    }
  12.  
    }
  13.  
     
  14.  
    public static class InnerTest implements Cloneable {
  15.  
    public InnerTest2 test;
  16.  
     
  17.  
    public InnerTest(InnerTest2 test) {
  18.  
    this.test = test;
  19.  
    }
  20.  
     
  21.  
    @Override
  22.  
    public Object clone() throws CloneNotSupportedException {
  23.  
    return super.clone();
  24.  
    }
  25.  
    }
  26.  
     
  27.  
    public static class InnerTest2 implements Cloneable {
  28.  
    public InnerTest2() {
  29.  
    }
  30.  
     
  31.  
    @Override
  32.  
    public Object clone() throws CloneNotSupportedException {
  33.  
    return super.clone();
  34.  
    }
  35.  
    }
  36.  
    }

 

3. clone跳過構造函數

此外,clone方法不通過構造函數來創建新對象,所以構造函數內的邏輯也會被直接跳過,這也會帶來問題,等於clone引進了一個我們無法控制的對象構造方法。比如想在構造函數內實現一個計數功能,每次new就加1,但是如果clone的話,則這個計數就無法生效。比如:

 

  1.  
    public class Test implements Cloneable {
  2.  
     
  3.  
    public static void main(String[] args) {
  4.  
    try {
  5.  
    List<Cloneable> list = new ArrayList<Cloneable>();
  6.  
    InnerTest t1 = new InnerTest("test");
  7.  
    InnerTest t2 = new InnerTest("test1");
  8.  
    list.add(t1);
  9.  
    list.add(t2);
  10.  
    list.add((Cloneable) t1.clone());
  11.  
    for (Cloneable c : list) {
  12.  
    System.out.println(((InnerTest) c).index ); // 依次打印 0 1 0
  13.  
    }
  14.  
    System.out.println(InnerTest.count); // count為2
  15.  
    } catch (Exception e) {
  16.  
    e.printStackTrace();
  17.  
    }
  18.  
    }
  19.  
     
  20.  
    public static class InnerTest implements Cloneable {
  21.  
    public int index;
  22.  
    public static int count = 0;
  23.  
     
  24.  
    public InnerTest(String test) {
  25.  
    index = count;
  26.  
    count++;
  27.  
    }
  28.  
    public Object clone() throws CloneNotSupportedException {
  29.  
    return super.clone();
  30.  
    }
  31.  
    }
  32.  
    }

 

4. 最佳實踐——復制構造函數或者自定義Copyable接口

另外clone方法本身也是線程不安全的。所以總結下來就是clone是很不靠譜的,所以主流的建議還是添加復制構造函數,這樣雖然會比較麻煩一點,但是可控性強且可以實現deep copy。

 

此外也可以自己實現一套Copyable接口,然后想要復制的類都繼承該接口並復現copy函數即可。但是copy函數內的邏輯其實與復制構造類似。比如:

Copyable接口:

 

  1.  
    public interface Copyable<T> {
  2.  
    T copy ();
  3.  
    }

 

 

具體實現與測試:

 

  1.  
    public class Test{
  2.  
    public static void main(String[] args) {
  3.  
    try {
  4.  
    InnerTest t1 = new InnerTest(new InnerTest2());
  5.  
    InnerTest t2 = t1.copy();
  6.  
    System.out.println(t1.test.getA()); // print 0
  7.  
    t1.test.setA( 5);
  8.  
    System.out.println(t2.test.getA()); // print 0
  9.  
    } catch (Exception e) {
  10.  
    e.printStackTrace();
  11.  
    }
  12.  
    }
  13.  
     
  14.  
    // 測試類
  15.  
    public static class InnerTest implements Copyable<InnerTest> {
  16.  
    // set to public for convenience
  17.  
    public InnerTest2 test;
  18.  
     
  19.  
    public InnerTest(InnerTest2 tmp) {
  20.  
    this.test = tmp;
  21.  
    }
  22.  
     
  23.  
    @Override
  24.  
    public InnerTest copy() {
  25.  
    InnerTest2 tmp = test == null ? null : test.copy();
  26.  
    return new InnerTest(tmp);
  27.  
    }
  28.  
    }
  29.  
     
  30.  
    // 測試類,增加getter和setter方法來驗證
  31.  
    public static class InnerTest2 implements Copyable<InnerTest2>{
  32.  
    private int a;
  33.  
    public InnerTest2() {
  34.  
    a = 0;
  35.  
    }
  36.  
     
  37.  
    public int getA() {
  38.  
    return a;
  39.  
    }
  40.  
     
  41.  
    public void setA(int a) {
  42.  
    this.a = a;
  43.  
    }
  44.  
     
  45.  
    @Override
  46.  
    public InnerTest2 copy() {
  47.  
    InnerTest2 tmp = new InnerTest2();
  48.  
    tmp.setA( this.a);
  49.  
    return tmp;
  50.  
    }
  51.  
    }
  52.  
    }

 

 

3. 序列化實現深復制

1. 為什么使用序列化

其實大部分情況下復制構造是個不錯的選擇,但是實現上來說確實比較繁瑣,且容易出錯,因為需要遞歸式的將所有的對象和它引用的對象都進行復制,所以就有了另外一種實現deep copy的思路:Java Object Serialization (JOS)。序列化會將一個對象的各個方面都考慮到,包括父類,各個字段,以及各種引用。所以如果將一個對象先序列化寫入字節流,然后再讀出,重新構造成一個對象,就能實現這個對象的deep copy。當然,這里其實也沒考慮構造函數邏輯,但是這種方法卻不需要考慮會有shallow copy的可能,而且省去了繁瑣的復制構造或者copy方法的覆寫,我們可以直接通過一個實現一個deepCopy函數來實現對象復制。下面就對這種方法做一個介紹。

2. 深復制的實現

如何實現deepCopy函數,下面提供一個簡單的例子:
  1.  
    public class Test2 {
  2.  
    public static Object deepCopy(Object from) {
  3.  
    Object obj = null;
  4.  
    try {
  5.  
    // 將對象寫成 Byte Array
  6.  
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
  7.  
    ObjectOutputStream out = new ObjectOutputStream(bos);
  8.  
    out.writeObject(from);
  9.  
    out.flush();
  10.  
    out.close();
  11.  
     
  12.  
    // 從流中讀出 byte array,調用readObject函數反序列化出對象
  13.  
    ObjectInputStream in = new ObjectInputStream(
  14.  
    new ByteArrayInputStream(bos.toByteArray()));
  15.  
    obj = in.readObject();
  16.  
    } catch(IOException e) {
  17.  
    e.printStackTrace();
  18.  
    } catch(ClassNotFoundException e2) {
  19.  
    e2.printStackTrace();
  20.  
    }
  21.  
    return obj;
  22.  
    }
  23.  
    }
通過上面的例子,我們之間調用deepCopy函數就可以將一個對象進行deep copy並且返回一個新的對象。這里的writeObject和readObject分別將對象序列化和反序列化。

3.序列化存在的問題

這種方法看上去比較簡單,但是其實仍然存在很多問題:
首先,想要實現序列化必須實現序列化接口,也就表示所有需要深復制的類都應該實現Serializable接口,不過這倒是比較容易解決。
第二,序列化操作比較慢,其實序列化和反序列化兩個操作是比較耗時的,這雖然可以通過自己來實現一套writeObject和readObject來解決,但是這里始終都是瓶頸。
第三,序列化操作中ByteArrayInputStream和ByteArrayOutputStream是線程安全的,一般情況下這沒什么問題,但是當本身業務中不涉及到多線程情況的話這就會拖慢deep copy的速度。
其中第二點實現比較麻煩且速度提升不明顯,但是在不涉及多線程的情況下,第三條卻可以得到改變,我們可以自己實現非線程安全的InputStream和OutputStream的子類去替換ByteArrayInputStream和ByteArrayOutputStream,從而提升速度。
 

4. 使用相關第三方庫

前面說到的幾種方案都是各有優缺點,要么就是實現比較繁瑣,要么就是功能不夠穩定,一般這個時候可以看下是否有相關功能的成熟的類庫,事實是關於deep copy的第三方庫很多,比如Dozer(https://github.com/DozerMapper/dozer),Kryo(https://github.com/EsotericSoftware/kryo),cloning(https://github.com/kostaskougios/cloning)等,使用成熟類庫可以很快且高效的實現deep copy,具體的發放此處不贅述,直接看github上文檔即可。
 
總結一下,實現deep copy,主要的方法有:
  1. 實現Cloneable接口並覆寫clone方法
  2. 使用復制構造函數
  3. 自定義一個Copyable接口,然后為需要clone的類增加copy方法的具體實現
  4. 通過序列化方式將一個對象先序列化再反序列化得到一個deep copy的新對象
  5. 使用成熟第三方庫,具體方法看文檔。
原文章出處:https://blog.csdn.net/hzycaicai2012/article/details/45564443
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM