Java中的clone()方法

clone()方法的约定

Cloneable接口的目的是作为对象的一个混合接口,表明这样的对象允许克隆(clone),但是这个接口却没有定义clone(),这是它的缺陷:无法约束子类实现clone()方法。Object定义了一个受保护的clone()方法。Cloneable虽然没有定义clone()方法,但是却影响了Object.clone()方法的行为:如果一个类实现了Cloneable,调用Object的clone()就会返回该对象的逐域拷贝,否则抛出CloneNotSupportedException。这真是一种非常规的用法,Cloneable接口没有规定实现类的视图,却改变了父类受保护方法的行为。调用clone()会创建并返回对象的拷贝,看看JDK文档中对clone()方法的约定:

(1)x.clone() != x; 克隆对象与原对象不是同一个对象

(2)x.clone().getClass() == x.getClass(); 克隆的是同一类型的对象

(3)x.clone().equals(x) == true,如果x.equals()方法定义恰当的话

注意,上面的三条规则要求不是绝对的,一般来说前两条是必需的,第三个也应该尽量遵守。

实现Cloneable接口的类和其所有超类都必需遵守一个复杂、不可实施、且没有文档说明的协议,由此得到一种语言之外的机制:无需调用构造器就可以创建对象。然而,“不调用构造器”的规定有些僵硬,行为良好的clone()方法可以调用构造器创建对象,比如final类,它不会有子类,所以在它的clone()方法中调用构造器创建对象是一种合理的选择。

使用clone()的规则

“如果你覆盖了非final类中的clone方法,则应该返回一个通过调用super.clone()而得到的对象”,这是使用clone()方法的规则,如果不遵守这条规则,在clone()方法中调用了构造器,那么就会得到错误的类。如代码所示:

class A implements Cloneable
{   //类A的clone()直接调用构造器
	public A clone() {
		return new A();
	}
}
 
class B extends A
{
	public B clone() {
		try
		{
			return (B) super.clone();
		}
		catch (CloneNotSupportedException e)
		{
			throw new AssertionError();
		}
	}
}

类 B 的 clone() 方法就不会得到正确的对象,因为 super .clone() 返回的是使用 A 的构造器创建的类 A 的对象。如果类 B 的 clone 方法想得到正确的对象,那么 A 的 clone 方法应该这样写:

public A clone() {
	try{
		return (A) super.clone();
	}catch (CloneNotSupportedException e){
		throw new AssertionError();
	}
}

由此,我们可以看出调用super.clone()最终会调用Object类的clone方法,前提是子类的所有超类都遵循了上面的规则,否则无法实施。注意,A和B的clone方法的返回值不必是Object,Java1.5引入了协变返回类型作为泛型,覆盖方法的返回值可以是被覆盖方法返回值的子类。

Cloneable实在是一个失败的接口,它并没有指明实现它的类需要承担哪些责任,通常情况下,实现Cloneable的类应当提供一个功能适当的公有的clone()方法。

浅克隆

克隆出来的对象的所有变量含有与原来的对象相同的值,而对其他对象的引用都指向原来的对象。也就是说,浅克隆仅仅克隆所考虑的对象。Object的clone就是"shallow copy"。如果类的每个域都是基本类型的值,或者是指向不可变对象的引用,那么调用Object.clone()就能得到正确的对象。

/**
 *如果每个域都是基本类型,或者指向不可变对象的引用
 *那么这个类只需要声明实现Cloneable接口,提供公有的clone()方法
 */
class ShallowCopy implements Cloneable
{
	private String name;
	private int no;
 
	public ShallowCopy(String name,int no) {
		this.name = name;
		this.no = no;
	}
 
	/*只需调用super.clone()就能得到正确的行为*/
	public ShallowCopy clone() {
		try
		{
			return (ShallowCopy)super.clone();
		}
		catch (CloneNotSupportedException e)
		{
			throw new AssertionError();
		}
	}
}

通常情况下,我们已经得到了正确的对象,但是如果类里面包含代表序列号或者唯一ID的域,或者创建时间的域,还需要对这些域进行修正。

深克隆

深克隆把引用域所指向的对象也克隆一遍。考虑下面这样一个类:

class Person
{
	private Dog friend;
	public Person(Dog dog) {
		friend = dog;
	}
}
class Dog
{
	private String name;
	public Dog(String name) {
		this.name = name;
	}
}

Person类的friend域不是基本类型,而是指向了可变的对象,这个时候如果调用Object.clone()进行浅克隆,那么克隆出来的对象的friend指向的还是原来的dog,就是说:

Person p = new Person(new Dog(“金毛”));

p.clone().friend== p.friend;//true

p.clone().friend.name = “狼狗”;

p.friend.name.equals(“狼狗”);//true,改变克隆对象,却同时更改了原对象

实际上,clone方法是另一种构造器:你必须确保不会伤害到原来的对象。为了使Person的clone方法正确工作,也要对friend进行克隆,最简单的做法就是调用friend.clone():

public Person clone() {
		try{
			Person result = (Person) super.clone();
			result.friend = friend.clone();
		}
		catch (CloneNotSupportedException e){
			throw new AssertionError();
		}
}

可是,如果friend域是final的,那么上面的clone()也无法正常工作,因为super.clone()时已经给friend赋一次值了,不能再去修正克隆对象的friend域了。这是个根本问题:clone架构与引用可变对象的final域的正常用法是不相兼容的!

抛去final域的问题不谈,递归的调用clone()方法就解决问题了吗?问题在于,深克隆要深入到哪一层,是一个不易确定的问题。考虑下面的类:

import java.util.Arrays;
/**
 *内部实现了单向链表
 *buckets里的每个元素保存一个单向链表
 *
 */
class NMap implements Cloneable
{
	private Entry[] buckets;
	
	public NMap(int size) {
		buckets = new Entry[size];
		for(int i = 0; i < size; i++)
			buckets[i] = new Entry("M10","Messi",new Entry("X6", "Xavi", null));
	}
 
	public Entry[] getBuckets() {
		return buckets;
	}
 
   static class Entry
	{
		final Object key;
		Object value;
		Entry next;
 
		Entry(Object key,Object value,Entry next) {
			this.key = key;
			this.value = value;
			this.next = next;
		}
 
		public void setNext(Entry next) {
			this.next = next;
		}
		
		public String toString() {
			String result =  key + ":" + value + "  ";
			if(next != null)
				result += next.toString();
			return result;
		}
	}
 
	public NMap clone(){
		try
		{
			NMap result = (NMap)super.clone();
			//数组被视为实现了Cloneable接口
			result.buckets = buckets.clone();
			return result;
		}catch (CloneNotSupportedException e){
			throw new AssertionError();
		}
	}
 
	public static void main(String[] args) {
		NMap map = new NMap(5);
	    
		System.out.println(Arrays.toString(map.getBuckets()));
		
		NMap clone = map.clone();
		Entry entry = new Entry("G4","Guadiorla",new Entry("R9","Ronaldo",null));
		for(Entry ent : clone.getBuckets())
			ent.setNext(entry);
		
		System.out.println(Arrays.toString(map.getBuckets()));
	}
}

运行这段代码之后会发现,虽然克隆对象有自己的数组buckets,但是数组中引用的链表与原始对象是一样的,修改克隆对象数组中的链表,原始对象中数组保存的对象也会随之而修改。解决这种问题的方法是在Entry类中增加一个“深度拷贝(deep copy)”方法。

public Entry deepEntry() {
	Entry result = new Entry(key,value,next);
	for(Entry p = result; p.next != null; p = p.next)
		p.next = new Entry(p.next.key,p.next.value,p.next.next);
	return result;
}
NMap的clone()方法如下:
public NMap clone(){
	try
	{
		NMap result = (NMap)super.clone();
		//数组被视为实现了Cloneable接口
		result.buckets = buckets.clone();
		for(int i = 0; i < buckets.length; i++)
			if(buckets[i] != null)
				result.buckets[i] = buckets[i].deepEntry();
		return result;
	}catch (CloneNotSupportedException e){
		throw new AssertionError();
	}
}

克隆复杂对象还有一种方法,先调用super.clone()得到类型正确的对象,然后把所有域都设置成空白状态,然后调用高层的方法重新产生对象的状态。这种做法会产生一个简单、合理且相当优美的clone方法,运行速度稍慢。

总结

1.Cloneable接口是一个失败的接口,它没有提供clone()方法,却影响了Object.clone()克隆的行为:如果类没有实现Cloneable接口,调用super.clone()方法会得到CloneNotSupportedException。

2.所有实现了Cloneable接口的类都应该提供一个公有的方法覆盖clone(),此公有方法首先调用super.clone(),然后修正域,此公有方法一般不应该声明抛出CloneNotSupportedException。

3.如果为了继承而设计的类不应该实现Cloneable接口,这样可以使子类具有实现或者不实现Cloneable接口的自由,就仿佛它们直接扩展了Object一样。父类没有实现Cloneable接口,也没有覆盖clone(),子类如果实现了Cloneable,在覆盖的clone()中调用super.clone()是可以得到正确对象的。

据说很多专家级程序猿从来都不使用clone()方法。

更好的方法

等等,为了实现clone()方法的功能,有必要这么复杂吗?很少有这种必要。为了实现对象拷贝的更好的方法是提供一个拷贝构造器或者拷贝工厂,它们接受这类的一个对象作为参数。

public Yum( Yum yum);//拷贝构造器

public static YumcopyInstance(Yum yum);//拷贝工厂

  • 10
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值