1. 克隆的用處
在日常編碼中我們經常需要産生某個對象的副本,這裡的副本并不是指向同一個對象的不同引用,而是與目前對象狀态一模一樣的另一個新的對象。如果使用單純的引用指派,會發生什麼效果呢?
我們可以觀察下面的代碼:
package com.coderap.foundation.clone;
class Address {
public String province;
public String city;
public Address(String province, String city) {
this.province = province;
this.city = city;
}
@Override
public String toString() {
return "Address{" +
"province='" + province + '\'' +
", city='" + city + '\'' +
'}';
}
}
class Person {
public String name;
public Integer age;
public Address address;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
public class CloneTest {
public static void main(String[] args) {
Person person = new Person("Tom", 20);
person.address = new Address("CA", "Los Angeles");
System.out.println("before: " + person.name);
System.out.println("before: " + person.age);
System.out.println("before: " + person.address);
Person newPerson = (Person) person;
person.name = "Jack";
person.age = 22;
newPerson.address.province = "CA";
newPerson.address.city = "Nevada";
System.out.println("after: " + person.name);
System.out.println("after: " + person.age);
System.out.println("after: " + person.address);
System.out.println("person equal newPerson ? " + person.equals(newPerson));
}
}
在上面的代碼中我們單純地将一個新的引用指向一個已有的對象,然後使用新的引用對對象進行操作,可以發現,所有的更改在兩個引用上都展現出來了:
before: Tom
before: 20
before: Address{province='CA', city='Los Angeles'}
after: Jack
after: 22
after: Address{province='CA', city='Nevada'}
person equal newPerson ? true
在Java中,兩個引用同時指向相同的對象時,這兩個引用是指向的同一塊記憶體,是以使用任何一個引用對記憶體的操作都将直接反映到另一個引用上,單純的引用指派是不能夠克隆對象的。為了解決克隆問題,Java提供了Cloneable接口和clone()方法。
2. Cloneable 接口和 clone 方法
Cloneable接口是一個标記接口,其中沒有任何内容,定義如下:
package java.lang;
public interface Cloneable {}
clone()方法是在Object類中定義的:
protected native Object clone() throws CloneNotSupportedException;
clone()方法是被protected修飾的受保護的方法,類隻有實作了Cloneable接口,才可以在該類的執行個體上調用clone()方法,否則會抛出CloneNotSupportException異常。
Object的clone()方法建立并傳回此對象的一個副本。對于任何對象o,clone()方法有以下的規則:
o.clone() != o為true;
o.clone().getClass() == o.getClass()為true;
o.clone().equals(o)一般情況下為true,但這并不是必須要滿足的要求。
Object中預設的實作是一個淺克隆,但是該方法是有缺陷的,如果需要實作深層次克隆的話,必須對類中可變域生成新的執行個體。
2.1. 淺克隆
淺克隆并不會把對象所有屬性全部克隆一份,而是有選擇性的克隆,克隆規則如下:
基本類型。如果變量是基本類型,則克隆其值,比如int、float、long等。
String字元串,對于字元串的克隆比較特殊,克隆的是引用位址,但是在修改的時候,它會從字元串池(String Pool)中重新生成新的字元串,原有的字元串對象保持不變,此處可以認為String是個基本類型。
對象。如果變量時一個執行個體對象,則克隆位址引用,也就是說此時新克隆出的對象與原有對象共享該執行個體變量,不受通路權限的限制。這中克隆操作是非常危險的,意味着不同的對象之間對某些引用對象是共有的,互相修改将受到影響。
注1:基本資料類型在克隆時是進行的原值克隆。
如下面的代碼,我們隻是簡單的在Person類中實作了Cloneable接口并且重寫了clone()方法,同時進行克隆操作:
package com.coderap.foundation.clone;
class Address {
public String province;
public String city;
public Address(String province, String city) {
this.province = province;
this.city = city;
}
@Override
public String toString() {
return "Address{" +
"province='" + province + '\'' +
", city='" + city + '\'' +
'}';
}
}
class Person implements Cloneable {
public String name;
public Integer age;
public Address address;
public Person() {
System.out.println("Person() execute");
}
public Person(String name, Integer age) {
System.out.println("Person(String name, Integer age) execute");
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
Person person = new Person("Tom", 20);
person.address = new Address("CA", "Los Angeles");
System.out.println("before: " + person.name);
System.out.println("before: " + person.age);
System.out.println("before: " + person.address);
Person newPerson = (Person)person.clone();
newPerson.name = "Jack";
newPerson.age = 22;
newPerson.address.province = "CA";
newPerson.address.city = "Nevada";
System.out.println("after: " + person.name);
System.out.println("after: " + person.age);
System.out.println("after: " + person.address);
System.out.println("person != newPerson ? " + String.valueOf(person != newPerson));
System.out.println("person getClass equal newPerson getClass ? " + person.getClass().equals(newPerson.getClass()));
System.out.println("person equal newPerson ? " + person.equals(newPerson));
}
}
運作上面的代碼,可以得到列印資訊如下:
Person(String name, Integer age) execute
before: Tom
before: 20
before: Address{province='CA', city='Los Angeles'}
after: Tom
after: 20
after: Address{province='CA', city='Nevada'}
person != newPerson ? true
person getClass equal newPerson getClass ? true
person equal newPerson ? false
我們可以得出以下結果:
克隆一個對象不會重複調用對應類的構造方法;
上述最後的三條的判斷的結果是遵循了clone()方法三條規則的;
基本類型和String類型的資料都是獨立的,并不會收到新對象的影響,但是引用類型的對象會受到新對象的影響。
需要注意的是,在修改城市資訊時,如果我們直接指定newPerson.address = new Address("CA", "Nevada")其實是不會影響到原來的person對象的,因為雖然newPerson和person的address指向的同一個Address對象,但使用newPerson.address = new Address("CA", "Nevada")會給newPerson對象生成一個新的Address對象,并将newPerson的address引用指向這個新的對象,是以并不會影響到原有的person對象的address對象屬性。
Java中實作了Cloneable接口的類有很多,如ArrayList、Calendar、Date、HashMap、Hashtable、HashSet、LinkedList等等。我們在使用這些類時并不需要考慮淺克隆帶來的影響。
2.2. 深克隆
深克隆操作應該将除自身對象以外的所有對象,包括自身所包含的所有對象執行個體都進行克隆。
其實Object的clone()方法提供的是一種淺克隆的機制,如果想要實作對對象的深克隆,有兩種辦法:
先對對象進行序列化,緊接着馬上反序列化出;
先調用super.clone()方法克隆出一個新對象來,然後在子類的clone()方法中手動給克隆出來的非基本資料類型(引用類型)指派,比如ArrayList的clone()方法:
public Object clone() {
try {
ArrayList> v = (ArrayList>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}