Java中的物件“克隆”

__HelloWorld__發表於2018-08-27

前言

前一章節中,我們討論了構建Java物件的五種方式,其中,clone(克隆)也是我們比較常見的一種方式。

protected native Object clone() throws CloneNotSupportedException;

如果想要克隆一個物件,我們需要:

  • 實現Cloneable介面,否則當我們呼叫clone方法時JVM將會丟擲CloneNotSupportedException異常
  • 包含clone方法,用以處理CloneNotSupportedException異常
  • 我們通過呼叫父類的clone()方法來執行克隆操作,注意這是一個鏈式呼叫操作,當我們呼叫父類的clone()方法,父類會呼叫父類的clone()方法,直至Object類的clone()方法,實際上是Object類的clone方法執行了你所定義物件的克隆操作,我們定義的clone方法實際是委託給當前父類的clone操作。

物件克隆的分類

物件克隆有淺拷貝和深拷貝兩種分類,下面我們例子說明兩者區別。

class Person implements Cloneable {
    private String name; 
    private int income; 
    private City city; 
    public String getName() {
        return name;
    }
    public void setName(String firstName) {
        this.name = firstName;
    }
    public int getIncome() {
        return income;
    }
    public void setIncome(int income) {
        this.income = income;
    }
    public City getCity() {
        return city;
    }
    public void setCity(City city) {
        this.city = city;
    }
    public Person(String firstName, int income, City city) {
        super();
        this.name = firstName;
        this.income = income;
        this.city = city;
    }
    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", income=" + income + ", city=" + city + "]";
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((city == null) ? 0 : city.hashCode());
        result = prime * result + income;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        if (city == null) {
            if (other.city != null)
                return false;
        } else if (!city.equals(other.city))
            return false;
        if (income != other.income)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
}

注意,Person類本身包含一個引用型變數City

class City implements Cloneable {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public City(String name) {
        super();
        this.name = name;
    }
    public City clone() throws CloneNotSupportedException {
        return (City) super.clone();
    }
    @Override
    public String toString() {
        return "City [name=" + name + "]";
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        City other = (City) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
}

下面我們來測試下物件克隆

public class CloningExample {
    public static void main(String[] args) throws CloneNotSupportedException {
        City city = new City("Dehradun");
        Person person1 = new Person("Naresh", 10000, city);
        System.out.println(person1);
        Person person2 = person1.clone();
        System.out.println(person2);
        if (person1 == person2) { 
            System.out.println("Both person1 and person2 holds same object");
        }
        if (person1.equals(person2)) { 
            System.out.println("But both person1 and person2 are equal and have same content");
        }
        if (person1.getCity() == person2.getCity()) {
            System.out.println("Both person1 and person2 have same city object");
        }
    }
}

物件克隆是指將一個物件的內容逐位複製到另一個物件,這意味著一個物件的所有例項變數的值將被複制到其他物件的例項變數中去。

因此:

  • person1==person2 =》 false,記憶體地址不一致
  • person1.equals(person2) =》 true,內容副本,內容一致
  • person1.getCity() == person2.getCity() =》true,引用型變數,引用型變數本身存在各自棧空間中,實際指向堆中的同一個物件,因此結果為true。

這就是我們Object.clone的預設克隆策略:淺拷貝,如果我們想執行深拷貝,那麼我們需要改造下Person類的clone方法:


public Person clone() throws CloneNotSupportedException {
    Person clonedObj = (Person) super.clone();
    clonedObj.city = this.city.clone();
    return clonedObj;
}

那麼在這個前提下,person1.getCity() == person2.getCity()將返回false,因為他們指向的是堆中不同的物件。

實現方式

我們已經知道,在Java中物件克隆分兩種:淺克隆(淺拷貝)與深克隆(拷貝),本章,我們將討論如何具體實現物件克隆,這裡我們討論兩種方式:

  • 建構函式拷貝:Copy Constructors
  • Object.clone()

首先,我們來看Object.clone()的優缺點。

Object.clone

優點

  • 程式碼簡潔(淺拷貝方式),但注意:如果需要深拷貝,則需要重寫clone方法
  • 實現簡單,特別是當我們需要針對已研發或上線的舊系統做改造,我們只需要實現一個公共父類,並且提供一個公共clone方法,所以的子類自動繼承。
  • 對於陣列物件來說,克隆是拷貝陣列最快的方式。
  • 自JDK1.5之後,針對陣列上的克隆無需額外的型別轉換。

缺點

  • 使用Object.clone()需要我們程式碼中增加需要額外的語法:實現Cloneable介面,定義clone()方法體,處理 CloneNotSupportedException異常,最後呼叫Object.clone()方法,並轉換為我們程式所需要的物件
  • Cloneable介面本身不包含clone方法,Cloneable本身只是一個標識介面,其本身不包含任何方法,但我們仍舊需要繼承實現它以告訴JVM我們的物件支援Clone操作。
  • 在JDK中Object.clone()方法被定義為protected,注意,這是一種包訪問和繼承訪問修飾符,所以一般情況下(對於非java.lang包中的類),我們無法直接呼叫Object.clone方法,只能通過呼叫鏈的方式來間接呼叫
  • Object.clone()本身是native的,這是一種本地方法實現,所以我們無法控制物件的構建
  • 如果我們在子類中定義了clone()方法,那麼如果要實現支援物件克隆,那麼子類的所有父類必須直接或間接的實現clone方法,否則super.clone()呼叫鏈就會失效。
  • Object.clone只支援淺拷貝,所以拷貝物件中的引用型變數仍然指向原始引用型變數所指物件,引用型變數所指物件的改變會直接影響兩個父類持有者。所以,為了消除這種影響,你需要額外的程式碼實現深拷貝邏輯。
  • 無法修改final型別欄位,final型別欄位只能通過建構函式來修改。比如對於Person中id欄位,業務場景中肯定是需要id標識唯一,但事實上,我們通過Object.clone()獲取到的是重複物件。

因此,因為這些設計問題的存在,所以多數開發人員更樂意去採取別的方式來拷貝物件,比如:

注意,以上這些實現方式都需要我們引入額外的函式庫,事實上,這些函式庫內部實現也是呼叫序列化或者拷貝建構函式的方式實現,如果我們不希望引入額外庫,那麼我們可以通過以下方式自己實現物件克隆。

  • 序列化
  • 拷貝建構函式

序列化技術我們在此不多贅述,這裡我們看下拷貝建構函式方式。


public Person(Person original) {
    this.id = original.id + 1;
    this.name = new String(original.name);
    this.city = new City(original.city);
}

拷貝建構函式

優點

  • 無需我們強制實現任意介面或者處理任何異常,當然如果需要隨時可增加實現。
  • 無需任何型別實現。
  • 無需依賴任何黑盒物件建立方式,物件建立可控可見。
  • 無需父類必須遵循或者實現合約。
  • 支援修改final型別欄位
  • 物件建立可控可見,我們可以根據實際業務場景定製初始化邏輯。

此外,通過使用拷貝建構函式策略,我們還可以建立轉換建構函式,通過轉換建構函式,我們可以很方便的將一個物件轉換為另一個物件,比如:

 ArrayList(Collection<? extends E> c)

通過該建構函式,可以將任意Collection物件轉換為ArrayList物件。

相關文章