关于Java的Object.clone()方法与对象的深浅拷贝


文章同步更新在个人博客:关于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;

/**
 * 浅拷贝测试
 * 
 * @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()引出的深拷贝与浅拷贝,学习记录一下。

参考文章