Java浅谈克隆clone

为什么需要克隆clone

若需修改一个对象,同时不想改变调用者的对象,就要制作该对象的一个本地副本。这里我们讨论的是对象,如果是基本数据类型,就很简单了,只需要重新定义一个变量名称然后赋值即可。如果是一个对象,可能有些人说了,我直接new一个新的对象就可以了,这确实是一种解决方式,可是有一些在开发中需要使用的对象经过若干逻辑其中的属性早已经不再是初始值了。如果new不行,直接重新声明一个新名称使用"="赋值当然也不可以,对象如果使用"="赋值,那么两个对象在内存中会指向同一个地址,最终结果就是新对象所做的任何改动都会影响原对象中的值。制作本地副本简单的一种方式就是使用Java自身提供的clone()方法,该方法位于Object类中,权限修饰符是protected。Clone就是克隆的意思,即制作一个一模一样的副本。克隆有最常见的有两种方式即:深克隆和浅克隆,有时也叫做深复制和浅复制。

克隆clone简介

浅克隆

被克隆对象中所有变量的值都含有和原来对象相同的值,请记住这里所说的是值都是相同的。如果原对象中的变量是基本数据类型的,该变量会复制一份跟克隆对象,但是如果是引用类型的话,则会将该引用类型的地址复制一份给克隆对象,原对象和克隆对象中该成员变量的指向相同的内存地址。也就是说如果修改克隆对象中引用类型的值,同样会影响原对象中引用类型的值,反之原对象修改引用类型的值,克隆对象中引用类型值也会跟随着改变。

在Java中通过Object的clone()方法默认实现的就是浅克隆。

深克隆

类似浅克隆,被克隆对象中所有变量的值都含有和原来对象相同的值,不仅仅值是相同的,这里如果成员变量是引用类型的话,该引用类型的对象也会同样复制一份,也就是说不仅对象本身会复制一份,对象所持有的所有成员变量本身也都会复制一份。深克隆的对象如果成员变量是引用类型的话,一旦该引用类型的值被改变不会影响原对象中对应类型的值,同样如果原对象中引用类型值改变也不会影响克隆对象中的值,也就是说深克隆的时候除了克隆对象本身值看上去跟原对象相同之外不再具有任何关系。

在Java中有两种方式可实现深克隆,一种是通过Object的clone()方法,另外一种就是通过Serializable接口。

Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。

克隆clone方法

clone方法将对象复制了一份并返回给调用者。一般而言,clone()方法满足:

  • 对任何的对象x,都有x.clone() !=x//克隆对象与原对象不是同一个对象
  • 对任何的对象x,都有x.clone().getClass()= =x.getClass()//克隆对象与原对象的类型一样
  • 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。

如何实现对象的克隆

  • 在子类中覆盖Object类的clone()方法,并声明为public。
  • 在子类的clone()方法中,调用super.clone()。
  • 在子类中实现Cloneable接口。

克隆示例

使用clone方法

使用克隆方法既可以实现浅克隆,也可以实现深克隆。首先看一个浅克隆的示例,然后逐步深入进行深克隆,先定义一个地址类Address,然后再定义一个学生类Student,在学生类Student中包含了一个地址类Address的引用,我们让学生类Student实现Cloneable接口,然后重写clone()方法,并将clone()方法访问控制符更改为public。

public class Address{
	
	private String address;

	public String getAddress() {
		return address;
	}

	public void setAddress(String address) {
		this.address = address;
	}
	
	@Override
	public String toString() {
		return address;
	}
}
public class Student implements Cloneable {

	private int number;
	private Address address;

	public int getNumber() {
		return number;
	}

	public void setNumber(int number) {
		this.number = number;
	}

	public Address getAddress() {
		return address;
	}

	public void setAddress(Address address) {
		this.address = address;
	}

	@Override
	public Object clone() {
		try {
			return super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return null;
	}
	
	@Override
	public String toString() {
		return "Student [number=" + number + ", address=" + address + "]";
	}

}

Student student=new Student();
student.setNumber(1001);

Address address=new Address();
address.setAddress("street01");
student.setAddress(address);

Student cloneStu=(Student) student.clone();
cloneStu.setNumber(1002);

cloneStu.getAddress().setAddress("street02");

System.out.println(student);//Student [number=1001, address=street02]
System.out.println(cloneStu);//Student [number=1002, address=street02]

从运行的结果可以看出,克隆后的Student对象对地址Address更改后影响了原对象,这种结果很显然不是我们想要的,这种方式就是所说的浅克隆。那么如何才能够让克隆后的对象使用引用时不影响原对象呢?事实上还是使用clone()方法,这时候需要原对象中的每一个非基本数据类型的所对应的类都要实现Cloneable接口,并且重写clone()方法,这种方式就是使用clone()方法实现的深克隆,从这里可以看出来一个弊端,如果原对象中一个属性是引用类型,但是该引用类型中还包含另外一种引用类型,以此类推,如果使用clone()方式就会相当麻烦,在重写clone()方法的时候就会导致调用一层又一层,很容易出现问题。这也解释了为什么会有另外一种使用序列化Serializable进行深度克隆的问题,因为使用序列化Serializable的时候不需要重写clone()方法了。

按照上面的思路继续使用clone()来实现深克隆,首先将Address类实现Cloneable接口,然后重写clone()方法。

public class Address implements Cloneable{
	
	...
	@Override
	protected Object clone(){
		try {
			return super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return null;
	}

}

然后在Student类中对每一种引用类型都对克隆后的对象进行重新赋值。

public class Student implements Cloneable {

	...
	@Override
	public Object clone() {
		Student stu = null;
		try {
			stu = (Student) super.clone();
			if (null != address) {
				stu.address = (Address) address.clone();
			}
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return stu;
	}

}

然后我们再重新运行结果如下:

Student [number=1001, address=street01]
Student [number=1002, address=street02]

使用序列化Serializable克隆

使用序列化Serializable进行克隆一定是执行的深克隆,上面也进行了简单分析,如果纯粹的使用clone()方法进行深克隆,一旦我们要克隆的对象嵌套太多层次执行起来就相当繁琐,而使用序列化Serializable进行深克隆并不关心该对象有多少层引用嵌套。简单的同时就是比较耗时,而clone()方法我们点击查看源代码可以知道它实际上是一个native方法,特点就是执行起来比较快。但是在使用Serializable类时很虽然很容易设置,但在复制它们时却要做多得多的工作,虽然可以不写入文件,但是它确确实实是一个IO操作,耗时是难免的,这也从侧面说明该方式实现clone()应用并不是非常广泛。依赖其操作的简单性,开发中偶尔还是非常有用的,Apache的common-lang包下有一个类SerializationUtils,提供了专门的方法供Serializable序列化深克隆使用。

继续使用上面的示例进行介绍,我们将两个类进行序列化,不需要再实现Cloneable接口了,也不需要重写clone()方法,然后使用如下方法进行深度克隆即可。

public static  T clone(final T object) {
	if (object == null) {
		return null;
	}
	try {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(baos);
		oos.writeObject(object);
		// 将流序列化成对象
		ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
		ObjectInputStream in = new ObjectInputStream(bais);
		@SuppressWarnings("unchecked")
		final T readObject = (T) in.readObject();
		return readObject;

	} catch (final ClassNotFoundException e) {
		e.printStackTrace();
	} catch (final IOException e) {
		e.printStackTrace();
	}
	return null;
}
Student student=new Student();
student.setNumber(1001);

Address address=new Address();
address.setAddress("street01");
student.setAddress(address);

Student cloneStu=Utils.clone(student);
cloneStu.setNumber(1002);

cloneStu.getAddress().setAddress("street02");

System.out.println(student);//Student [number=1001, address=street01]
System.out.println(cloneStu);//Student [number=1002, address=street02]

参考资料

JAVA深复制(深克隆)与浅复制(浅克隆)

Java提高篇——对象克隆(复制)

SerializationUtils.java

理解Java中的协变返回类型

评论

您确定要删除吗?删除之后不可恢复