(基礎系列)object clone 的用法、原理和用途

猿青木發表於2017-09-18

前言

object clone(物件克隆)網上資料很多,那我為什麼還要寫下這篇文章呢?主要是想匯聚多篇文章的優秀之處以及我對於物件克隆的理解來加深印象,也使讀者能更全面的理解物件克隆的用法、原理和用途。

一、何謂 “object clone”

顧名思義clone就是一個相同東西的副本,是一個具體存在的複製體,是一個從生物科學開始變得熟悉的術語。在計算機行業,該術語被廣泛用於指Compaq,戴爾等人對IBM PC的模仿。而在java語言中,clone方法被物件呼叫,所以會複製物件。

二、object clone的用法

(1)方法摘要

作用域 型別 方法 描述
protected Object clone() 克隆實現了Cloneable介面的物件

注意事項:clone方法是被native修飾的,簡單的講就是被Native修飾的方法在被呼叫時指向的是一個非java程式碼的具體實現,這個實現可能是其他語言或者作業系統。

(2)clone規則:

1、 基本型別  
    如果變數是基本型別,則拷貝其值,比如int、float等。
2、 物件  
    如果變數是一個例項物件,則拷貝其地址引用,也就是說新物件和原來物件是共用例項變數的。
3、 String字串  
    若變數為String字串,則拷貝其地址引用。但是在修改時,它會從字串池中重新生成一個新的字串,原有的物件保持不變。複製程式碼

(2)示例1:

實現clone方法的步驟:
1. 實現Cloneable介面 
2. 過載Object類中的clone()方法,過載時需定義為public 
3. 在過載方法中,呼叫super.clone()複製程式碼
public class Book implements Cloneable {

    private int id;

    private String name;

    public Book() {}

    public Book(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

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

    public static void main(String[] args) throws CloneNotSupportedException {
        Book book1 = new Book();
        book1.setName("基礎系列1");
        Book book2 = (Book) book1.clone();

        System.out.println("圖書1:" + book1.getName());
        System.out.println("圖書2:" + book2.getName());

        book2.setName("基礎系列2");

        System.out.println("圖書1:" + book1.getName());
        System.out.println("圖書2:" + book2.getName());

    }
}複製程式碼

執行結果:

圖書1:基礎系列1
圖書2:基礎系列1
圖書1:基礎系列1
圖書2:基礎系列2複製程式碼

從執行結果看這應該是深克隆的,但為什麼是淺克隆呢?從string不可變(原物件和克隆物件中的string屬性引用的是同一地址)的角度出發結果應該是淺克隆,但從結果出發卻又是深克隆,所以從這一角度來說clone對string是深克隆。

注意事項:如果沒有implements Cloneable的類呼叫Object.clone()方法就會丟擲CloneNotSupportedException

(3)示例2:

public class Book implements Cloneable {

    //在示例1的基礎上增加bookBorrow的引用
    private BookBorrow bookBorrow;

    public Book() {}

    public Book(int id, String name, BookBorrow bookBorrow) {
        this.id = id;
        this.name = name;
        this.bookBorrow = bookBorrow;
    }

    public BookBorrow getBookBorrow() {
        return bookBorrow;
    }

    public void setBookBorrow(BookBorrow bookBorrow) {
       this.bookBorrow = bookBorrow;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        Book book = (Book)super.clone();
        //這裡註釋掉就是淺克隆,否則就是深克隆
        book.bookBorrow = (BookBorrow)bookBorrow.clone();
        return book;
    }

    @Override
    public String toString() {
        return "BOOK[id="+id+",name="+name+",bookBorrow:"+bookBorrow+"]";
    }

    public static void main(String[] args) throws CloneNotSupportedException {

        BookBorrow bookBorrow = new BookBorrow(1,1);
        Book book1 = new Book(1,"基礎系列1",bookBorrow);
        Book book2 = (Book) book1.clone();

        System.out.println("圖書1:" + book1.toString());
        System.out.println("圖書2:" + book2.toString());

        book2.setName("基礎系列2");
        book2.setBookBorrow(new BookBorrow(5,5));

        System.out.println("圖書1:" + book1.toString());
        System.out.println("圖書2:" + book2.toString());

    }
}

public class BookBorrow  implements Cloneable{

    private int id;

    private int borstate;


    public BookBorrow(int id, int borstate) {
        this.id = id;
        this.borstate = borstate;
    }

    public int getId() {
        return id;
    }

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

    public int getBorstate() {
        return borstate;
    }

    public void setBorstate(int borstate) {
        this.borstate = borstate;
    }

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

    @Override
    public String toString() {
        return "BookBorrow[id="+id+",borstate="+borstate+"]";
    }

}複製程式碼

執行結果:

圖書1:BOOK[id=1,name=基礎系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
圖書2:BOOK[id=1,name=基礎系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
圖書1:BOOK[id=1,name=基礎系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
圖書2:BOOK[id=1,name=基礎系列2,bookBorrow:BookBorrow[id=5,borstate=5]]複製程式碼

從結果看這裡是一個標準的深克隆實現,深克隆實現的一個主要前提是當前物件引用的物件或物件的物件引用的物件都實現了常規用法1並且在過載clone方法中呼叫其引用物件的clone方法。

例:

 @Override
    public Object clone() throws CloneNotSupportedException {
        Book book = (Book)super.clone();
        //這裡註釋掉就是淺克隆,否則就是深克隆
        book.bookBorrow = (BookBorrow)bookBorrow.clone();
        return book;
    }複製程式碼

注意事項:示例2給出的例子是相對簡單且常見的類,在實際開發中clone的物件可能依賴第三方的jar包或者引用層級過深不好修改的物件,如果是這種情況則建議採用示例3的做法,使用序列化clone。

(3)示例3:

序列化clone類

public class CloneUtils {

    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;
    }
}

public class BookBorrow implements Serializable{
    ...
    //去掉clone方法,繼承Serializable

}

public class Book implements Serializable {
    ...
    //去掉clone方法,繼承Serializable

    public static void main(String[] args) throws CloneNotSupportedException {

        BookBorrow bookBorrow = new BookBorrow(1,1);
        Book book1 = new Book(1,"基礎系列1",bookBorrow);
        Book book2 = CloneUtils.clone(book1);

        System.out.println("圖書1:" + book1.toString());
        System.out.println("圖書2:" + book2.toString());

        book2.setName("基礎系列2");
        book2.setBookBorrow(new BookBorrow(5,5));

        System.out.println("圖書1:" + book1.toString());
        System.out.println("圖書2:" + book2.toString());

    }
}複製程式碼

執行結果:

圖書1:BOOK[id=1,name=基礎系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
圖書2:BOOK[id=1,name=基礎系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
圖書1:BOOK[id=1,name=基礎系列1,bookBorrow:BookBorrow[id=1,borstate=1]]
圖書2:BOOK[id=1,name=基礎系列2,bookBorrow:BookBorrow[id=5,borstate=5]]複製程式碼

序列化克隆無需繼承,通過序列化工具類可實現深克隆同等效果。然而克隆沒有銀彈,序列化這種方式在效率上比之原clone有所不如

二、object clone原理

本次講解將基於示例1做出解釋:

為了不丟失上下文而貼出的測試程式碼,將會以2部分講解object clone的原理

public static void main(String[] args) throws CloneNotSupportedException {
    //第一部分
    Book book1 = new Book();
    book1.setName("基礎系列1");
    Book book2 = (Book) book1.clone();

    System.out.println("圖書1:" + book1.getName());
    System.out.println("圖書2:" + book2.getName());

    //第二部分
    book2.setName("基礎系列2");

    System.out.println("圖書1:" + book1.getName());
    System.out.println("圖書2:" + book2.getName());

}複製程式碼

第一部分執行結果

圖書1:基礎系列1
圖書2:基礎系列1複製程式碼

淺克隆原理圖:

image
image

從圖中可以看出clone的name引用的是同一個值,那為什麼前面又說是深克隆呢?原因就是在這一步中並沒有修改name所以他們是淺克隆,引用的是同一個name變數值。那接下來執行第二部分得出的結果和原理圖如你所想物件完全隔離了。

第二部分執行結果

圖書1:基礎系列1
圖書2:基礎系列2複製程式碼

深克隆原理圖:

image
image

從圖可以看出修改了name屬性值,clone會從堆中重新生成一個物件被克隆物件引用,而原物件保持不變,從這一角度出發的確是深克隆。

clone原理小結 :

前面的原理介紹是以示例1做為藍本介紹的,示例2 的原理和示例1類似,唯一區別是多了屬性物件而屬性物件在clone中也只會拷貝引用地址,要想實現深克隆就只能在引用的物件或引用物件的物件中中新增clone方法實現即可實現深克隆。

三、object clone的實際用途

1、精心設計一個淺克隆物件被程式快取,作為功能模組模板;每次有使用者呼叫這個模組則將可變部分替換成使用者需要的資訊即可。
示例:
功能:發郵件
描述:給同組的使用者傳送郵件,郵件內容相同(不可變)傳送的使用者不同(可變)

2、精心設計一個深克隆物件本程式快取,作為功能模組的初始物件,例如:“遊客模式”每個遊客進入系統訪問的都是初始物件,基於初始物件發展出多條變化不一的遊覽路線。只要你想的到設計巧妙,很多功能都能應用object clone。

四、總結

本文分3部分介紹了object clone,分別介紹了clone的用法、原理和用途; object clone歸結就是可變和不可變兩個特性,在實際的開發中我們可以基於這2個特性設計出效能良好的功能模組。

相關文章