Java Clone

本文部分内容节选自 Effective Java by Joshua Bloch.

 

   Cloneable接口的目的是作为一个mixin接口,表明实现这个接口的类的对象允许克隆。但是Cloneable接口本身并没有包含任何方法,但是它决定了Object的clone方法的行为:如果一个类实现了Cloneable,则Object的clone方法返回该对象的逐域拷贝(通常所说的浅拷贝,而且在这个过程中没有调用构造函数);否则的话,它抛出一个CloneNotSupportedException异常。 在Object类中,clone方法被定义成protected native Object clone() throws CloneNotSupportedException; 实际上,clone方法可以看成是另一个构造函数。Clone方法的通用约定是非常弱的,以下是java.lang.Object规范(javadoc)的内容:      

  1. x.clone() != x will be true
  2. x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements
  3. x.clone().equals(x) will be true, but these are not absolute requirements

   对于规定x.clone().getClass() == x.getClass(),在一个类的继承体系中,超类能提供这种功能的唯一途径是返回一个通过调用super.clone()方法返回的对象。如果一个类的所有超类都遵守这条规则,那么最终会调用到Object的clone方法,从而创建出正确的类的实例。因此,如果你改写了一个非final类的clone方法,则应该返回一个通过调用super.clone而得到的对象。

public class Base implements Cloneable {
    private String x;

    public Object clone() throws CloneNotSupportedException {
	    return super.clone();
    }

    public String getX() {
	    return x;
    }

    public void setX(String x) {
	    this.x = x;
    }
}

public class Derived extends Base implements Cloneable {
	private String y;
	
	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}

	public String getY() {
		return y;
	}

	public void setY(String y) {
		this.y = y;
	}
	
	public static void main(String args[]) throws CloneNotSupportedException {
		Derived d1 = new Derived();
		d1.setX("x");
		d1.setY("y");
		Derived d2 = (Derived)d1.clone();
		System.out.println("d2.x: " + d2.getX() + ", d2.y: " + d2.getY());
	}
}

    如果一个超类返回了由构造函数创建的对象,它将是具有错误的类。如下所示:

public class Base implements Cloneable {
    private String x;

    public Object clone() throws CloneNotSupportedException {
        Base base = new Base();
        base.x = this.x;
        return base;
    }

    public String getX() {
	    return x;
    }

    public void setX(String x) {
	    this.x = x;
    }
}

    假设你希望在一个类中实现Cloneable接口,并且它的超类都提供了良好的clone方法,那么你从super.clone方法中的到的对象可能会接近于最终想要的返回的对象,也可能相差甚远,这取决于这个类的本质。如果这个类的成员变量是原始类型或者是指向非可变对象的引用,那么返回的对象可能是你期望的对象。否则,可能需要进一步的处理。设想在Base类中声明一个private String[] elements;成员变量,如果不进行进一步的处理,那么克隆后对象的elements引用和被克隆对象的elements引用实际指向相同的String数组。最容易的解决方法是,对于elements数组递归调用clone方法。由于String对象是非可变对象,因此数组元素不需要再进行clone。

public Object clone() throws CloneNotSupportedException{
	Object r = super.clone();
	((Base)r).elements = this.elements.clone();
	return r;
}

    注意,如果elements成员变量是final的,则这种方案不能正常工作。
    如同构造函数一样,clone方法内不应该调用新对象上任何非final方法。如果调用了新对象上的非final方法,而且这个方法被子类改写过,那么这个被子类改写的方法就可能面对一个没有完全克隆完毕的对象,从而造成错误。Object的clone方法声明可以抛出CloneNotSupportedException异常,子类的改写版本可以忽略这个声明。但是对于一个为了继承而设计的类来说,保留这个声明是合适的。这样做可以是子类通过提供下面的方法,选择温和地放弃克隆的能力。

public Object clone() throws CloneNotSupportedException{
	throw new CloneNotSupportedException();
}

    简而言之,所有实现了Cloneable接口的类都应该用一个公共的clone方法改写超类的clone方法。此clone方法首先调用super.clone方法,然后修正任何需要修正的成员变量。通常情况下,这意味这着深拷贝任何可变对象。
    既然Cloneable接口具有以上那么多问题(与final成员变量发生冲突,强制客户捕获CloneNotSupportedException异常等),所以考虑一下实现对象拷贝的其他方法。一种通常的做法是提供一个C++中常见的拷贝构造函数(copy constructor)例如:

public Derived(Derived d);

     或者一个微小的变形,提供一个静态工厂来代替构造函数,例如:

public static Derived newInstance(Derived d);

     还有一种通常的做法是使用commans-beanutils提供的java bean拷贝工具。例如BeanUtils.copyProperties()和BeanUtils.cloneBean()。这两个方法执行的是浅拷贝。需要注意的是,在copyProperties的过程中会调用相应的Converter进行转换,例如BigDecimalConverter的convert方法如下:

public Object convert(Class type, Object value) {

    if (value == null) {
        if (useDefault) {
            return (defaultValue);
        } else {
            throw new ConversionException("No value specified");
        }
    }

    if (value instanceof BigDecimal) {
        return (value);
    }

    try {
        return (new BigDecimal(value.toString()));
    } catch (Exception e) {
        if (useDefault) {
            return (defaultValue);
        } else {
            throw new ConversionException(e);
        }
    }

}

    从以上代码中可以知道,如果被covert的值是null,而且BigDecimalConverter中没有使用缺省值,那么convert方法会抛出ConversionException。解决方法是在convert前调用ConvertUtils.register(new BigDecimalConverter(null), BigDecimal.class); 为BigDecimal类注册一个converter,并使用缺省值null,从而避免抛出异常。由于ConvertUtils上注册的converter是全局的,因此注册了这个converter之后,会影响所有对BigDecimal的covert方式。如果不确定这样做的后果,那么可以采用如下的代码,创建一个局部的BeanUtilsBean来负责拷贝。

ConvertUtilsBean cub = new ConvertUtilsBean();
cub.register(new BigDecimalConverter(null), BigDecimal.class);
BeanUtilsBean bub = new BeanUtilsBean(cub, new PropertyUtilsBean());
bub.copyProperties(dst, src);

你可能感兴趣的:(java,clone)