1. 程式人生 > >Java物件的深複製和淺複製

Java物件的深複製和淺複製

原文連結

我們在編碼過程經常會碰到將一個物件傳遞給另一個物件,java中對於基本型變數採用的是值傳遞,而對於物件比如bean傳遞時採用的引用傳遞也就是地址傳遞,而很多時候對於物件傳遞我們也希望能夠象值傳遞一樣,使得傳遞之前和之後有不同的記憶體地址,在這種情況下我們一般採用以下兩種情況。

淺複製與深複製概念

淺複製(淺克隆) :被複制物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用仍然指向原來的物件。換言之,淺複製僅僅複製所考慮的物件,而不復制它所引用的物件。

深複製(深克隆) :被複制物件的所有變數都含有與原來的物件相同的值,除去那些引用其他物件的變數。那些引用其他物件的變數將指向被複制過的新物件,而不再是原有的那些被引用的物件。換言之,深複製把要複製的物件所引用的物件都複製了一遍。

Java的clone()方法

clone方法將物件複製了一份並返回給呼叫者。一般而言,clone()方法滿足:

①對任何的物件x,都有x.clone() !=x//克隆物件與原物件不是同一個物件;
②對任何的物件x,都有x.clone().getClass()= =x.getClass()//克隆物件與原物件的型別一樣 ;
③如果物件x的equals()方法定義恰當,那麼x.clone().equals(x)應該成立;

Java中物件的克隆:

①為了獲取物件的一份拷貝,我們可以利用Object類的clone()方法。
②在派生類中覆蓋基類的clone()方法,並宣告為public。
③在派生類的clone()方法中,呼叫super.clone()。
④在派生類中實現Cloneable介面。

package com.king.cloneable;

/**
 * 淺複製
 */
public class ShallowStudent implements Cloneable {
    private String name;

    private int age;

    ShallowStudent(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Object clone() {
        ShallowStudent o = null
; try { // Object中的clone()識別出你要複製的是哪一個物件。 o = (ShallowStudent) super.clone(); } catch (CloneNotSupportedException e) { System.out.println(e.toString()); } return o; } public static void main(String[] args) { ShallowStudent s1 = new ShallowStudent("zhangsan", 18); ShallowStudent s2 = (ShallowStudent) s1.clone(); s2.name = "lisi"; s2.age = 20; //修改學生2後,不影響學生1的值。 System.out.println("name=" + s1.name + "," + "age=" + s1.age); System.out.println("name=" + s2.name + "," + "age=" + s2.age); } }

Java的所有類都預設繼承java.lang.Object類,在java.lang.Object類中有一個方法clone()。 JDK API的說明文件解釋這個方法將返回Object物件的一個拷貝。要說明的有兩點:一是拷貝物件返回的是一個新物件,而不是一個引用。二是拷貝物件與用 new操作符返回的新物件的區別就是這個拷貝已經包含了一些原來物件的資訊,而不是物件的初始資訊。

上面程式碼中有三個值得注意的地方,一是希望能實現clone功能的CloneClass類實現了Cloneable介面,這個介面屬於java.lang包, java.lang包已經被預設的匯入類中,所以不需要寫成java.lang.Cloneable。另一個值得請注意的是過載了clone()方法。最後在clone()方法中呼叫了super.clone(),這也意味著無論clone類的繼承結構是什麼樣的,super.clone()直接或間接呼叫了java.lang.Object類的clone()方法。

下面再詳細的解釋一下這幾點:

應該說第三點是最重要的,仔細觀察一下Object類的clone()一個native方法,native方法的效率一般來說都是遠高於 java中的非native方法。這也解釋了為什麼要用Object中clone()方法而不是先new一個類,然後把原始物件中的資訊賦到新物件中,雖然這也實現了clone功能。對於第二點,也要觀察Object類中的clone()還是一個protected屬性的方法。這也意味著如果要應用 clone()方法,必須繼承Object類,在Java中所有的類是預設繼承Object類的,也就不用關心這點了。然後過載clone()方法。還有一點要考慮的是為了讓其它類能呼叫這個clone類的clone()方法,過載之後要把clone()方法的屬性設定為public。

那麼clone類為什麼還要實現Cloneable介面呢?稍微注意一下,Cloneable介面是不包含任何方法的!其實這個介面僅僅是一個標誌,而且這個標誌也僅僅是針對Object類中clone()方法的,如果clone類沒有實現Cloneable介面,並呼叫了Object的 clone()方法(也就是呼叫了super.Clone()方法),那麼Object的clone()方法就會丟擲 CloneNotSupportedException異常。

說明:

①為什麼我們在派生類中覆蓋Object的clone()方法時,一定要呼叫super.clone()呢?在執行時刻,Object中的clone()識別出你要複製的是哪一個物件,然後為此物件分配空間,並進行物件的複製,將原始物件的內容一一複製到新物件的儲存空間中。
②繼承自java.lang.Object類的clone()方法是淺複製。以下程式碼可以證明之。

package com.king.cloneable;

/**
 * 淺複製2
 */
public class ShallowStudent2 implements Cloneable {
    String name;// 常量物件。
    int age;
    Professor p;// 學生1和學生2的引用值都是一樣的。

    ShallowStudent2(String name, int age, Professor p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }

    public Object clone() {
        ShallowStudent2 o = null;
        try {
            o = (ShallowStudent2) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        return o;
    }

    public static void main(String[] args) {
        Professor p = new Professor("wangwu", 50);
        ShallowStudent2 s1 = new ShallowStudent2("zhangsan", 18, p);
        ShallowStudent2 s2 = (ShallowStudent2) s1.clone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age);
        System.out.println("name=" + s2.p.name + "," + "age=" + s2.p.age);
        //輸出結果學生1和2的教授成為lisi,age為30。
    }
}

class Professor {
    String name;
    int age;

    Professor(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

從中可以看出,呼叫Object類中clone()方法產生的效果是:先在記憶體中開闢一塊和原始物件一樣的空間,然後原樣拷貝原始物件中的內容。對基本資料型別,這樣的操作是沒有問題的,但對非基本型別變數,我們知道它們儲存的僅僅是物件的引用,這也導致clone後的非基本型別變數和原始物件中相應的變數指向的是同一個物件。大多時候,這種clone的結果往往不是我們所希望的結果,這種clone也被稱為”影子clone”。

那應該如何實現深層次的克隆,即修改s2的教授不會影響s1的教授?程式碼改進如下。 改進使學生1的Professor不改變(深層次的克隆):

package com.king.cloneable;

/**
 * 深複製
 */
public class DeepStudent implements Cloneable {
    String name;// 常量物件。
    int age;
    DeepProfessor p;// 學生1和學生2的引用值都是一樣的。

    DeepStudent(String name, int age, DeepProfessor p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }

    public Object clone() {
        DeepStudent o = null;
        try {
            o = (DeepStudent) super.clone();
            //對引用的物件也進行復制
            o.p = (DeepProfessor) p.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        return o;
    }

    public static void main(String[] args) {
        DeepProfessor p = new DeepProfessor("wangwu", 50);
        DeepStudent s1 = new DeepStudent("zhangsan", 18, p);
        DeepStudent s2 = (DeepStudent) s1.clone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age);
        System.out.println("name=" + s2.p.name + "," + "age=" + s2.p.age);
        //輸出結果學生1和2的教授成為lisi,age為30。
    }
}

class DeepProfessor implements Cloneable {
    String name;
    int age;

    DeepProfessor(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Object clone() {
        DeepProfessor o = null;
        try {
            o = (DeepProfessor)super.clone();
        } catch(CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        return o;
    }
}

JDK中StringBuffer型別,關於StringBuffer的說明,StringBuffer沒有過載clone()方法,更為嚴重的是StringBuffer還是一個 final類,這也是說我們也不能用繼承的辦法間接實現StringBuffer的clone。如果一個類中包含有StringBuffer型別物件或和 StringBuffer相似類的物件,我們有兩種選擇:要麼只能實現影子clone,要麼就在類的clone()方法中加一句(假設是 SringBuffer物件,而且變數名仍是p): o.p = new StringBuffer(p.toString()); //原來的是:o.p = (DeepProfessor) p.clone();

還要知道的是除了基本資料型別能自動實現深度clone以外,String物件是一個例外,它clone後的表現好象也實現了深度clone,雖然這只是一個假象,但卻大大方便了我們的程式設計。

通過以上我們可以看出在某些情況下,我們可以利用clone方法來實現物件的深度複製,但對於比較複雜的物件(比如物件中包含其他物件,其他物件又包含別的物件…..)這樣我們必須進行層層深度clone,每個物件需要實現cloneable介面,比較麻煩,那就繼續學習下一個序列化方法。

利用序列化來做深複製

所謂物件序列化就是將物件的狀態轉換成位元組流,以後可以通過這些值再生成相同狀態的物件。這個過程也可以通過網路實現,可以先在Windows機器上建立一個物件,對其序列化,然後通過網路發給一臺Unix機器,然後在那裡準確無誤地重新?裝配?。是不是很神奇。

也許你會說,只瞭解一點點,但從來沒有接觸過,其實未必如此。RMI、Socket、JMS、EJB你總該用過一種吧,彼此為什麼能夠傳遞Java物件,當然都是物件序列化機制的功勞。

第一次使用Java的物件序列化是做某專案,當時要求把幾棵非常複雜的樹(JTree)及相應的資料儲存下來(就是我們常用的儲存功能),以便下次執行程式時可以繼續上次的操作。

那時XML技術在網上非常的熱,而且功能也強大,再加上樹的結構本來就和XML儲存資料的格式很像。作為一項對新技術比較有興趣的我當然很想嘗試一下。不過經過仔細分析,發現如果採用XML儲存資料,後果真是難以想象:哪棵樹的哪個節點被展開、展開到第幾級、節點當前的屬性是什麼。真是不知該用A、B、C還是用1、2、3來表示。

還好,發現了Java的物件序列化機制,問題迎刃而解,只需簡單的將每棵樹的根節點序列化儲存到硬碟上,下次再通過反序列化後的根節點就可以輕鬆的構造出和原來一模一樣的樹來。

其實儲存資料,尤其是複雜資料的儲存正是物件序列化的典型應用。最近另一個專案就遇到了需要對非常複雜的資料進行存取,通過使用物件的序列化,問題同樣化難為簡。

物件的序列化還有另一個容易被大家忽略的功能就是物件複製(Clone),Java中通過Clone機制可以複製大部分的物件,但是眾所周知,Clone有深層Clone和淺層Clone,如果你的物件非常非常複雜,假設有個100層的Collection(誇張了點),如果你想實現深層 Clone,真是不敢想象,如果使用序列化,不會超過10行程式碼就可以解決。

主要是為了避免重寫比較複雜物件的深複製的clone()方法,也可以程式實現斷點續傳等等功能。把物件寫到流裡的過程是序列化(Serilization)過程,但是在Java程式師圈子裡又非常形象地稱為“冷凍”或者“醃鹹菜(picking)”過程;而把物件從流中讀出來的並行化(Deserialization)過程則叫做 “解凍”或者“回鮮(depicking)”過程。

應當指出的是,寫在流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面,因此“醃成鹹菜”的只是物件的一個拷貝,Java鹹菜還可以回鮮。

在Java語言裡深複製一個物件,常常可以先使物件實現Serializable介面,然後把物件(實際上只是物件的一個拷貝)寫到一個流裡(醃成鹹菜),再從流裡讀出來(把鹹菜回鮮),便可以重建物件。

如下為深複製原始碼:

public Object deepClone() {    
   //將物件寫到流裡    
   ByteArrayOutoutStream bo=new ByteArrayOutputStream();    
   ObjectOutputStream oo=new ObjectOutputStream(bo);    
   oo.writeObject(this);    
   //從流裡讀出來    
   ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());    
   ObjectInputStream oi=new ObjectInputStream(bi);    
   return(oi.readObject());    
}

這樣做的前提是物件以及物件內部所有引用到的物件都是可序列化的,否則,
就需要仔細考察那些不可序列化的物件或屬性可否設成transient,從而將之排除在複製過程之外。上例程式碼改進如下。

package com.king.cloneable;

import java.io.*;

/**
 * 通過序列化實現深複製
 */
class Teacher implements Serializable {
    String name;
    int age;

    public Teacher(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class DeepStudent2 implements Serializable {
    String name;//常量物件
    int age;
    Teacher t;//學生1和學生2的引用值都是一樣的。

    public DeepStudent2(String name, int age, Teacher t) {
        this.name = name;
        this.age = age;
        this.t = t;
    }

    public Object deepClone() throws IOException, ClassNotFoundException {//將物件寫到流裡
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(this);//從流裡讀出來
        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return (oi.readObject());
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Teacher t = new Teacher("tangliang", 30);
        DeepStudent2 s1 = new DeepStudent2("zhangsan", 18, t);
        DeepStudent2 s2 = (DeepStudent2) s1.deepClone();
        s2.t.name = "tony";
        s2.t.age = 40;
        //學生1的老師不改變
        System.out.println("name=" + s1.t.name + "," + "age=" + s1.t.age);
    }
}

雖然Java的序列化非常簡單、強大,但是要用好,還有很多地方需要注意。比如曾經序列化了一個物件,可由於某種原因,該類做了一點點改動,然後重新被編譯,那麼這時反序列化剛才的物件,將會出現異常。

你可以通過新增serialVersionUID屬性來解決這個問題。如果你的類是個單態(Singleton)類,是否允許使用者通過序列化機制複製該類,如果不允許你需要謹慎對待該類的實現。

相關推薦

Java物件複製複製

原文連結 我們在編碼過程經常會碰到將一個物件傳遞給另一個物件,java中對於基本型變數採用的是值傳遞,而對於物件比如bean傳遞時採用的引用傳遞也就是地址傳遞,而很多時候對於物件傳遞我們也希望能夠象值傳遞一樣,使得傳遞之前和之後有不同的記憶體地址,在這種情況下

物件複製複製的區別。

  場景   當你的元件裡需要用到同一個資料,但身負不同的責任。   舉個例子:vue的雙向繫結   你在與後端的互動中請求回來的資料,res,  let a = res; let b = res;  因為是會用到checkbox,在外層的列表裡選中資

Java中clone方法以及複製複製

Java中處理基本資料型別(如:int , char , double等),都是採用按值傳遞的方式執行,除此之外的其他型別都是按照引用傳遞(傳遞的是一個物件的引用)的方式執行。物件在函式呼叫時和使用“=”賦值時都是引用傳遞。 Java中clone方法的作用是為了在現實程式

C 複製複製

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

javascript的複製複製(深度拷貝拷貝)

<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>js深度複製淺顯複製</title> </head>

ES6(JavaScript)的複製複製

React中,我們會遇到一個有趣的問題,那就是物件的複製,為什麼說有趣,是因為直覺和結果差距很大。 我們看一下這個例子: let a={tile:'深複製'}; let b=a; a.title='淺複製'; 那麼我們會獲得兩個物件,一個a,一個b,a的title是淺複製,

題目筆記(閉包,複製複製,原生js實現Promise)

就面試筆試題的一些筆記: 閉包( 實現add(2)(5) ) 深複製和淺複製 原生js實現Promise △ –>閉包知識: 實現add(2)(5) function add (x) { return functio

如何理解原型模式中的複製複製

    找工作之前製作簡歷時,需要做很多份簡歷,而且簡歷的格式是一樣的,也就意味著要做很多重複性的工作。在程式設計過程中,遇到重複性工作多的時候,貼上複製是最快的解決辦法。但是一旦這些程式碼需要修改時

面試:C++的複製複製(轉)

物件的構造,也可以由複製建構函式完成,即用一個物件的內容去初始化另一個物件的內容。此時,若物件使用了堆空間(注意和“堆物件”區分),就有深、淺複製的問題,不清楚則很容易出錯。 什麼是淺複製 預設複製建構函式:用一個物件的內容初始化另一個同類物件,也稱為預設的

C 複製複製

                物件的複製   對於普通型別的物件來說,它們之間的複製是很簡單的,例如:int a=88;int b=a;double f=3.12;double d(f);  而類物件與普通物件不同,類物件內部結構一般較為複雜,存在各種資料成員。下面看一個類物件複製的簡單例子。#includ

C++複製複製

物件的複製   對於普通型別的物件來說,它們之間的複製是很簡單的,例如:int a=88; int b=a; double f=3.12; double d(f);  而類物件與普通物件不同,類物件內部結構一般較為複雜,存在各種資料成員。下面看一個類物件複製的簡單例子。#in

python的複製複製

# -*- coding:UTF-8 -*- #此次練習主要是討論物件的淺複製和深複製 import copy #淺複製一個object有三種方式:1、slice操作; # 2、copy模組的copy函式; #

java拷貝拷貝

淺拷貝: public class PrototypeClass implements Cloneable { private List<String> params = new ArrayList<String>(); @Over

物件拷貝拷貝

寫在前面 各類技術論壇關於深拷貝的部落格有很多,有些寫的也比我好,那為什麼我還要堅持寫這篇部落格呢,之前看到的一篇部落格中有句話寫的非常好 學習就好比是座大山,人們沿著不同的路登山,分享著自己看到的風景。你不一定能看到別人看到的風景,體會到別人的心情。只有自己去登山,才能看到不一樣的風景,體會才更加

Java拷貝拷貝(deepcopyshallowcopy)

原型模式要求用深拷貝(複製/克隆)的方式實現,對這個概念很模糊。在自己查了相關資料後,我將從三個方面講述深淺拷貝: 圖形表述深淺拷貝區別及特點 Object類中的clone()方法實現深淺拷貝 實際應用中深淺拷貝區別 一 圖形表述深淺拷貝區別及特點 前提:深淺拷貝基本

初探python物件複製問題的拷貝拷貝

前階段學習python時遇到物件拷貝的問題,感覺有個不小的坑,於是乎搜了一下網上相關部落格的介紹,然而總覺得敘述太長,不夠簡潔。本文通過總結前人經驗,並根據自己的理解,簡單談一談python中的拷貝小坑。 python中實現物件複製的方法大致有3種:簡單粗暴直

物件拷貝拷貝(複製複製

淺拷貝(淺複製)        在C++中經常隱式或顯式的出現物件的複製。當兩個物件之間進行復制時,若複製完成後,它們還共享某些資源(記憶體空間),其中一個物件的銷燬會影響另一物件,這種物件之間的複製稱為物件的淺拷貝。C++中採用賦值運算子進行物件複製是預設的是淺拷貝。 #

JAVA clone方法-複製(克隆)&複製(克隆)

引子 為啥要用clone方法? 最近在專案中發現某開發人員程式碼有問題,然而單元測試也確實不通過,就是對物件的引用失敗造成的 具體如下: 在對某個物件更新儲存資料操作,物件關聯某個檔案需要將物件更新到資料庫後再判斷檔案是否更新(檔案儲存到專門的檔案系統中,物件保持檔案的

Java中的複製複製

//******************************** 兩個物件指向同一記憶體堆 start ********************************* public class Client_v1 { Student s1=new Student(1,"LMGD",9

java 深度複製複製

Java淺度複製是值引用的複製,而不是值的複製。 java深度複製才是值的複製。  1.其中,Cloneable介面必須實現,且重寫該clone方法,重寫clone方法且改為範圍改成public