1. 程式人生 > >淺談Java的標識介面-cloneable

淺談Java的標識介面-cloneable

為什麼對物件進行克隆

談到了物件的克隆,就不得不說為什麼要對物件進行克隆?Java中所有的物件都是儲存在堆中,而堆是供全域性共享的。也就是說,如果同一個Java程式的不同方法,只要能拿到某個物件的引用,引用者就可以隨意的修改物件的內部資料(前提是這個物件的內部資料通過get/set方法曝露出來)。有的時候,我們編寫的程式碼想讓呼叫者只獲得該物件的一個拷貝(也就是一個內容完全相同的物件,但是在記憶體中存在兩個這樣的物件),有什麼辦法可以做到呢?當然是克隆咯。

clone()函式和cloneable介面
先來看一下他們在文件中的描述:
Cloneable:
這裡寫圖片描述

Clone():
這裡寫圖片描述

通過上述的描述,我們可以看出,要讓一個物件進行克隆,其實就是兩個步驟:

  1. 讓該類實現java.lang.Cloneable介面;
  2. 重寫(override)Object類的clone()方法。

例項

class User implements Cloneable
{
    int id;
    int age;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int
getAge() { return age; } public void setAge(int age) { this.age = age; } public User(int id,int age){ this.id = id; this.age = age; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } //一定要重寫,否則利用equals()會出錯
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (age != user.age) return false; if (id != user.id) return false; return true; } //一定要重寫,否則利用equals()會出錯. @Override public int hashCode() { int result = id; result = 31 * result + age; return result; } } public class CloneTest { public static void main(String[] args) throws CloneNotSupportedException { User user1 = new User(1,12); User user2 = user1; User user3 = (User) user1.clone(); System.out.println(user1==user2);//true System.out.println(user1.equals(user2));//true System.out.println(user1==user3);//false System.out.println(user1.equals(user3));//true } }

注:剛開始的時候只是重寫了clone(),致使System.out.println(user1.equals(user3));一直為false具體原因請參考淺談java的==,equals(),hashcode()

但是事實真的就這樣簡單了,請看下面的程式碼:

class Administrator implements Cloneable
{
    private User user;
    private boolean editable;

    public Administrator(User user,boolean editable){
        this.user = user;
        this.editable = editable;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public boolean isEditable() {
        return editable;
    }

    public void setEditable(boolean editable) {
        this.editable = editable;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Administrator that = (Administrator) o;

        if (editable != that.editable) return false;
        if (user != null ? !user.equals(that.user) : that.user != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = user != null ? user.hashCode() : 0;
        result = 31 * result + (editable ? 1 : 0);
        return result;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class CloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        Administrator a1 = new Administrator(new User(1,12),true);
        Administrator a2 = a1;
        Administrator a3 = (Administrator) a1.clone();

        System.out.println(a1==a2);//true
        System.out.println(a1.equals(a2));//true
        System.out.println(a1==a3);//false
        System.out.println(a1.equals(a3));//false
        System.out.println(a1.getUser()==a3.getUser());//true,不可思議了吧!!!
        System.out.println(a1.getUser().equals(a3.getUser()));//true
    }
}

出問題了吧!!!這裡我們就可以引入兩個專業的術語:淺克隆(shallow clone)和深克隆(deep clone)。

所謂的淺克隆,顧名思義就是很表面的很表層的克隆,如果我們要克隆Administrator物件,只克隆他自身以及他包含的所有物件的引用地址。
深克隆,就是非淺克隆。克隆除自身以外所有的物件,包括自身所包含的所有物件例項。至於深克隆的層次,由具體的需求決定,也有“N層克隆”一說。
但是,所有的基本(primitive)型別資料,無論是淺克隆還是深克隆,都會進行原值克隆。畢竟他們都不是物件,不是儲存在堆中。注意:基本資料型別並不包括他們對應的包裝類。

如果我們想讓物件進行深度克隆,我們可以這樣修改Administrator類。


    @Override
    protected Object clone() throws CloneNotSupportedException {
        Administrator a = (Administrator) super.clone();
        //在方法內部呼叫持有物件的clone()方法;
        a.user = (User) a.user.clone();
        return a;
    }

所以實現深度克隆的步驟如下:

  1. 讓該類實現java.lang.Cloneable介面;
  2. 確認持有的物件是否實現java.lang.Cloneable介面並提供clone()方法;
  3. 重寫(override)Object類的clone()方法,並且在方法內部呼叫持有物件的clone()方法;
  4. ……
  5. 多麻煩啊,調來調去的,如果有N多個持有的物件,那就要寫N多的方法,突然改變了類的結構,還要重新修改clone()方法。

首先編寫一個工具類:

public abstract class BeanUtil {
    @SuppressWarnings("unchecked")
    public static Administrator cloneTo(Administrator src){
        ByteArrayOutputStream memoryBuffer = new ByteArrayOutputStream();
        ObjectOutputStream out = null;
        ObjectInputStream in = null;
        Administrator dist = null;
        try {
            out = new ObjectOutputStream(memoryBuffer);
            out.writeObject(src);
            out.flush();
            in = new ObjectInputStream(new ByteArrayInputStream(memoryBuffer.toByteArray()));
            dist = (Administrator) in.readObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (out != null)
                try {
                    out.close();
                    out = null;
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            if (in != null)
                try {
                    in.close();
                    in = null;
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
        }
        return dist;
    }
}

使User,Administrator實現介面Serializable

public class CloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        Administrator a1 = new Administrator(new User(1,12),true);
        Administrator a3 = BeanUtil.cloneTo(a1);

        System.out.println(a1==a3);//false
        System.out.println(a1.equals(a3));//true
        System.out.println(a1.getUser()==a3.getUser());//false
        System.out.println(a1.getUser().equals(a3.getUser()));//true
    }
}

好了,無論你的物件有多麼的複雜,只要這些物件都能夠實現java.lang.Serializable介面,就可以進行克隆,而且這種克隆的機制是JVM完成的,不需要修改實體類的程式碼,方便多了。

為什麼這麼簡單就可以實現物件的克隆呢?java.lang.Serializable介面又是幹嘛用的呢?

**把物件寫到流裡的過程是序列化過程(Serialization),而把物件從流中讀出來的過程則叫做反序列化過程(Deserialization)。(上面的BeanUtil就是實現序列化反序列化的工具類.)
應當指出的是,寫在流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面。
在Java語言裡深複製一個物件,常常可以先使物件實現Serializable介面,然後把物件(實際上只是物件的一個拷貝)寫到一個流裡,再從流裡讀出來,便可以重建物件。
這樣做的前提是物件以及物件內部所有引用到的物件都是可序列化的,否則,就需要仔細考察那些不可序列化的物件可否設成transient,從而將其排除在複製過程之外。**