1. 程式人生 > >Java - 物件複製,cloneable與序列化複製的區別

Java - 物件複製,cloneable與序列化複製的區別

當需要對同一個類,生成多個物件時。一般有三種方法:new()、clone()、以及序列化複製

new和clone的區別,簡單的說一下:

new的操作為 分配記憶體。程式執行到new操作符時, 首先去看new操作符後面的型別,因為知道了型別,才能知道要分配多大的記憶體空間。分配完記憶體之後,再呼叫建構函式,填充物件的各個域,這一步叫做物件的初始化,構造方法返回後,一個物件建立完畢,可以把他的引用(地址)釋出到外部,在外部就可以使用這個引用操縱這個物件。

而clone在第一步是和new相似的, 都是分配記憶體,呼叫clone方法時,分配的記憶體和源物件(即呼叫clone方法的物件)相同,然後再使用原物件中對應的各個域,填充新物件的域, 填充完成之後,clone方法返回,一個新的相同的物件被建立,同樣可以把這個新物件的引用釋出到外部。

由此可見,clone的速度是大於new的,尤其是在對大物件操作時。

然而,我們知道拷貝分為深拷貝和淺拷貝

淺拷貝:將一個物件複製後,基本資料型別的變數都會重新建立,而引用型別,指向的還是原物件所指向的。

深拷貝:將一個物件複製後,不論是基本資料型別還有引用型別,都是重新建立的。

顯然,淺拷貝是不安全的,可不是我們想要的。

 

下面我們來看一下clone方法和序列化方法,對於深拷貝淺拷貝的區別差異:

先看一下clone方法的淺拷貝和深拷貝。

我們先定義一個Person類,用作引用型別。

public class Person{
    int i =0;
}

然後在寫一個需要拷貝物件的類。TextClass。並實現Cloneable介面的clone方法。

public class TextClass implements Cloneable
{
    private int age;
    private String name;
    final int i=9;
    private Person person;
    public TextClass clone(){
        TextClass textClass = null;
        try {
            textClass =(TextClass)super.clone();
        }catch (CloneNotSupportedException e) {
            System.out.println(e);
        }
        return textClass;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Person getPerson() {
        return person;
    }
    public void setPerson( Person person) {
        this.person = person;
    }
}

現在,我們執行程式碼來看看前後clone物件的值,是否相等。(完整程式碼在最後一起給出,此處為了方便理解,逐語句的驗證輸出)。我們先例項化了一個text1物件,作為原物件。之後通過clone來複製出一個textClass1物件。

        TextClass text1 =new TextClass();
        text1.setAge(40);
        text1.setName(new String("zhou"));
        Person person = new Person();
        text1.setPerson(person);

        TextClass textClass1 = text1.clone();
        System.out.println("1: person比較");
        System.out.println(text1.getPerson()==textClass1.getPerson());

分析,此次是淺複製,我們沒有對Person進行進一步的拷貝,賦值。所以輸出因為該true。

輸出結果為:true,無誤。

1: person比較
true

那麼該如何實現深複製呢?畢竟深複製才是我們想要的。這就必須對引用型別進行進一步的拷貝和賦值。修改之後的程式碼為:

public class Person implements Cloneable{
    int i =0;
    public Person clone(){
        Person person =null;
        try {
            person =(Person)super.clone();
        }catch(CloneNotSupportedException e){
            e.printStackTrace();
        }
        return  person;
    }
}
public class TextClass implements Cloneable
{
    private int age;
    private String name;
    final int i=9;
    private Person person;
    public TextClass clone(){
        TextClass textClass = null;
        try {
            textClass =(TextClass)super.clone();
            textClass.setPerson(this.getPerson().clone()); //加入的語句
        }catch (CloneNotSupportedException e) {
            System.out.println(e);
        }
        return textClass;
    }
    // set get...   
}

再次執行,上面的測試程式碼,如果正確,應該為false。因為此時的person已用了clone來複制,兩者指向的地址並不一樣。結果,無誤:

1: person比較
false

下面我們來看一下使用序列化的方式來複制時的情況

首先寫一個序列化方法類:

public class CloneUtils {
    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T clone(T obj){
        T cloneObj = null;
        try {
            //寫入位元組流
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream obs = new ObjectOutputStream(out);
            obs.writeObject(obj);
            obs.close();

            //分配記憶體,寫入原始物件,生成新物件
            ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(ios);
            //返回生成的新物件
            cloneObj = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return cloneObj;
    }
}

在進行測試之前,我們先要將Person和TextClass類實現介面 Serializable;以保證能夠序列化

public class TextClass implements Serializable,Cloneable{
    ...
}
public class Person implements Cloneable,Serializable{
   ...
}
測試程式碼為:
        TextClass text2 = CloneUtils.clone(text1);
        System.out.println("1: person比較");
        System.out.println(text1.getPerson()==text2.getPerson());

因為序列化是將物件寫進二進位制流中,然後經過反序列化,再從中讀取出來,所以應該是完全不一樣的兩個物件。

故而是一個徹底的深複製。實驗結果因為為 false。執行也是無誤

1: person比較
false

 

至此,我們可以小結一下,clone方法只能複製基本型別,對於引用型別它只是淺拷貝。遇到引用型別,我們必須要對該屬性(person)進行單獨的clone。當引用型別太多時,會多出很大的工作量。也真是因為這樣,所以引用型別一旦是final修飾時,我們就不能使用clone方法了。

序列化和反序列化,則是徹底的深拷貝。

 

在實驗的時候,還發現clone拷貝String變數的問題。先不說是為什麼,直接上測試程式碼。

        System.out.println("2: name比較");
        System.out.println(text1.getName()==textClass1.getName());

        text1.setName("xu");
        System.out.println(text1.getName());
        System.out.println(textClass1.getName());
        System.out.println("3: name比較");
        System.out.println(text1.getName()==textClass1.getName());

 大家猜一下實驗結果是什麼?是不是想說:

2: name比較
true
zhou
zhou
xu
xu
3: name比較
true

如果,你是這麼想的。那麼說明你關於clone是懂了。但是你執行後會發現,結果不是這樣的。

哈哈哈,驚喜不,意外不。

執行的結果是這樣的:

2: name比較
true
zhou
zhou
xu
zhou
3: name比較
false

前面四個輸出是沒有什麼問題的。第一個true,是因為String為引用型別,是淺複製。

問題出現在,對text1.setName(“xu”);之後,既然是淺複製為什麼textClass1.getName()不為“xu”;

這是因為String底層存放的資料是final的!

當你執行text1.setName(“xu”)、並不是將text1.Name指向的“zhou”改成“xu”;而是生成一個新的String物件“xu”;讓text1.Name指向“xu”;而textClass1.getName()=“zhou”;也證明了這一點,原來地址上的資料並沒有被修改。

最後一個false ,想必到現在你也知道了,是的,因為text1.Name指向的地址偷偷地變了,不再是原來存放“zhou”的地址,

而是新String物件“xu”的地址。