文章同步更新在個人博客:關於Java的Object.clone()方法與對象的深淺拷貝
引言
在某些場景中,我們需要獲取到一個對象的拷貝用於某些處理。這時候就可以用到Java中的Object.clone方法進行對象復制,得到一個一模一樣的新對象。但是在實際使用過程中會發現:當對象中含有可變的引用類型屬性時,在復制得到的新對象對該引用類型屬性內容進行修改,原始對象響應的屬性內容也會發生變化,這就是"淺拷貝"的現象。關於淺拷貝,Object.clone()方法的描述也有說明:
/**
* Creates and returns a copy of this object. The precise meaning
* ...
* The method {@code clone} for class {@code Object} performs a
* specific cloning operation. First, if the class of this object does
* not implement the interface {@code Cloneable}, then a
* {@code CloneNotSupportedException} is thrown. Note that all arrays
* are considered to implement the interface {@code Cloneable} and that
* the return type of the {@code clone} method of an array type {@code T[]}
* is {@code T[]} where T is any reference or primitive type.
* Otherwise, this method creates a new instance of the class of this
* object and initializes all its fields with exactly the contents of
* the corresponding fields of this object, as if by assignment; the
* contents of the fields are not themselves cloned. Thus, this method
* performs a "shallow copy" of this object, not a "deep copy" operation.
*
* 上面這里已經說明了,clone()方法是淺拷貝,而不是深拷貝
*
* ...
* @return a clone of this instance.
* @exception CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
*/
protected native Object clone() throws CloneNotSupportedException;
淺拷貝
接下來用代碼看看淺拷貝的效果。
1. 創建一個Person類
package com.test.objclone.shallow;
public class Person implements Cloneable {
private int age;
private String name;
private Address address;
public Person(int age, String name, Address address) {
this.age = age;
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String display() {
return "Person [age=" + age + ", name=" + name + ", address=" + address + "]";
}
}
由於clone()方法是protected修飾的,因此需要實現Cloneable接口才能調用,同時需要覆寫clone()方法才能調用。
2. 創建一個Address類
package com.test.objclone.shallow;
public class Address {
private String province;
private String street;
public Address(String province, String street) {
this.province = province;
this.street = street;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
@Override
public String toString() {
return "Address [province=" + province + ", street=" + street + "]";
}
}
3. 淺拷貝測試
package com.test.objclone.shallow;
/**
* 淺拷貝測試
* </ul>
* @author hanxiaojun
* @version 5.0 since 2018年3月14日
*/
public class MTest {
public static void main(String[] args) throws CloneNotSupportedException {
Person person = new Person(15, "zhangsan", new Address("四川", "天府二街"));
Person clonePerson = (Person) person.clone();
System.out.println(person);
System.out.println(clonePerson);
System.out.println(person.display());
System.out.println(clonePerson.display());
clonePerson.setName("wangwu");
clonePerson.setAge(20);
Address address = clonePerson.getAddress();
address.setStreet("天府四街");
System.out.println(clonePerson.display());
System.out.println(person.display());
}
}
以上程序結果如下:
com.test.objclone.shallow.Person@7ba28183
com.test.objclone.shallow.Person@69e4fede
Person [age=15, name=zhangsan, address=Address [province=四川, street=天府二街]]
Person [age=15, name=zhangsan, address=Address [province=四川, street=天府二街]]
Person [age=20, name=wangwu, address=Address [province=四川, street=天府四街]]
Person [age=15, name=zhangsan, address=Address [province=四川, street=天府四街]]
可以看到:
第1、2句輸出說明了原對象與新對象是兩個不同的對象。
第3、4句可以看到拷貝出來的新對象與原對象內容一致
但是,接着將新對象里面的信息進行了修改,然后輸出發現原對象里面的部分信息也跟着變了。仔細觀察發現原對象跟着變化的只是Address部分,這就跟clone本身的淺拷貝有關系了。
淺拷貝:創建一個新對象,然后將當前對象的非靜態字段復制到該對象,如果字段類型是值類型(基本類型)的,那么對該字段進行復制;如果字段是引用類型的,則只復制該字段的引用而不復制引用指向的對象。此時新對象里面的引用類型字段相當於是原始對象里面引用類型字段的一個副本,原始對象與新對象里面的引用字段指向的是同一個對象。
因此,修改clonePerson里面的address內容時,原person里面的address內容會跟着改變。
深拷貝
了解了淺拷貝,那么深拷貝是什么也就很清楚了。即將引用類型的屬性內容也拷貝一份新的。
那么,實現深拷貝我這里收集到兩種方式:第一種是給需要拷貝的引用類型也實現Cloneable接口並覆寫clone方法;第二種則是利用序列化。接下來分別對兩種方式進行演示。
深拷貝-clone方式
對於以上演示代碼,利用clone方式進行深拷貝無非就是將Address類也實現Cloneable,然后對Person的clone方法進行調整。
1. Address類變動
實現Cloneable,並覆寫clone方法
package com.test.objclone.deep.clone;
public class Address implements Cloneable{
private String province;
private String street;
public Address(String province, String street) {
this.province = province;
this.street = street;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
@Override
public String toString() {
return "Address [province=" + province + ", street=" + street + "]";
}
}
2. Person類變動
對clone方法進行如下調整:
package com.test.objclone.deep.clone;
public class Person implements Cloneable {
private int age;
private String name;
private Address address;
public Person(int age, String name, Address address) {
this.age = age;
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person person = (Person)super.clone();
//手動對address屬性進行clone,並賦值給新的person對象
person.address = (Address) address.clone();
return person;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String display() {
return "Person [age=" + age + ", name=" + name + ", address=" + address + "]";
}
}
3. 深拷貝測試
再次執行上面的測試方法
節省篇幅,就不再貼主方法測試類了
此時,測試代碼執行如下:
com.test.objclone.deep.clone.Person@69e4fede
com.test.objclone.deep.clone.Person@3918d722
Person [age=15, name=zhangsan, address=Address [province=四川, street=天府二街]]
Person [age=15, name=zhangsan, address=Address [province=四川, street=天府二街]]
Person [age=20, name=wangwu, address=Address [province=四川, street=天府四街]]
Person [age=15, name=zhangsan, address=Address [province=四川, street=天府二街]]
可以看到原person對象的address內容未受到影響。
但是,這種方法的缺點就是當一個類里面有很多引用類型時,需要手動調用很多clone,而且如果引用類型內部還有引用類型時,那么代碼將會很惡心,量也很大。。。
所以這種方式一般用於引用類型變量較少的時候。
對於很多引用類型,可以使用序列化對象的方式進行深拷貝。
深拷貝-序列化方式
這種方式其實就是將對象轉成二進制流,然后再把二進制流反序列成一個java對象,這時候反序列化生成的對象是一個全新的對象,里面的信息與原對象一樣,但是所有內容都是一份新的。
這種方式需要注意的地方主要是所有類都需要實現Serializable接口,以便進行序列化操作。
1. 序列化-反序列化對象
先看看核心代碼,序列化與反序列化對象:
package com.test.objclone.deep.serialize;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class DeepClone implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 利用序列化和反序列化進行對象的深拷貝
* @return
* @throws Exception
*/
protected Object deepClone() throws Exception{
//序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}
2. Person類的修改
主要是實現Serializable接口,讓對象支持序列化。
package com.test.objclone.deep.serialize;
public class Person extends DeepClone{
private static final long serialVersionUID = 1L;
private int age;
private String name;
private Address address;
public Person(int age, String name, Address address) {
this.age = age;
this.name = name;
this.address = address;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String display() {
return "Person [age=" + age + ", name=" + name + ", address=" + address + "]";
}
}
3. Address類的修改
也需要實現Serializable接口,為了方便就繼承DeepClone了。
package com.test.objclone.deep.serialize;
public class Address extends DeepClone{
private static final long serialVersionUID = 1L;
private String province;
private String street;
public Address(String province, String street) {
this.province = province;
this.street = street;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
@Override
public String toString() {
return "Address [province=" + province + ", street=" + street + "]";
}
}
4. 測試類
package com.test.objclone.deep.serialize;
public class MTest {
public static void main(String[] args) throws Exception {
Person person = new Person(15, "zhangsan", new Address("四川", "天府二街"));
Person clonePerson = (Person) person.deepClone();
System.out.println(person);
System.out.println(clonePerson);
System.out.println(person.display());
System.out.println(clonePerson.display());
clonePerson.setName("wangwu");
clonePerson.setAge(20);
Address address = clonePerson.getAddress();
address.setStreet("天府四街");
System.out.println(clonePerson.display());
System.out.println(person.display());
}
}
測試代碼執行結果如下:
com.test.objclone.deep.serialize.Person@43059849
com.test.objclone.deep.serialize.Person@6a5bc8c9
Person [age=15, name=zhangsan, address=Address [province=四川, street=天府二街]]
Person [age=15, name=zhangsan, address=Address [province=四川, street=天府二街]]
Person [age=20, name=wangwu, address=Address [province=四川, street=天府四街]]
Person [age=15, name=zhangsan, address=Address [province=四川, street=天府二街]]
可見,對新對象clonePerson的修改並沒影響原person對象的內容。
這便是由Object.clone()引出的深拷貝與淺拷貝,學習記錄一下。
參考文章