对Cloneable接口的讨论

2019.10.22

之前一直只知道浅拷贝深拷贝的问题,不知道Java的Cloneable居然有不小的设计问题,在此作详细讨论。文章主要基于 Effective Java 第13条。

简单来讲,要正确实现Cloneable,需要使所有实现了Cloneable接口的类覆盖clone方法,且设为public. 该方法应首先调用super.clone,然后修正任何需要修正的域。

Cloneable的主要问题在于,缺少一个clone方法而Object的clone方法是protected的。无法保证实现Cloneable的类一定有可调用的clone方法。Object中的clone方法会返回对象的逐域拷贝,而这绕过了构造函数,未实现Cloneable的话则会抛出一个CheckedException:CloneNotSupportedException. 而这使得Cloneable接口实际改变了父类中受保护的方法的行为。实现Cloneable接口的类为了提供一个功能适当的公有clone方法,必须实现一个复杂的机制。clone方法内部也无法给final域赋新值。

若希望在一个类中实现Cloneable接口,重写clone方法,若超类都提供了行为良好的clone方法,首先调用super.clone. 若每个域都是基本类型,或者包含一个指向不可变对象的引用,那么返回的对象大抵正是所需要的,不需要进一步处理。其中,不可变对象不应该提供clone方法,否则只会引发不必要的拷贝。

public class PhoneNumber implements Cloneable {
// 公有的clone方法应当省略throws声明,使得没有CheckedException
    @Override
    public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch (CloneNotSupportedException e) {
            // cannot happen
            throw new AssertionError();
        }
    }
}

与序列化相似,Cloneable与引用可变对象的final域的正常用法是不相兼容的,除非在原始对象和克隆对象之间可以安全地共享此可变对象。

书中提到,数组上调用clone返回的数组,其编译时的类型与被克隆数组的类型相同,数组相关的clone是clone方法唯一吸引人的用法。不过我感觉这也没什么用。

Clone的替代

避免自己或子类的clone可使用如下写法

@Override
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

应考虑使用其他途径来代替对象拷贝。可以使用拷贝构造器或拷贝工厂。

public PhoneNumber (PhoneNumber phoneNumber) {
        // ...
}
public static PhoneNumber newInstance(PhoneNumber phoneNumber) {
        // ...
}

使用上述方法可使得:1.不依赖于某一种具有风险的语言之外的对象创建机制;2.不要求遵守尚未制定好文档的规范;3.不会与final域的正常使用发生冲突;4.不会抛出不必要的CheckedException;5.不需要进行类型转换。

拷贝构造器和拷贝工厂灵活性较高,参数可以是某个类所实现的接口。因此,复制功能最好用上述方法替代,除了浅clone数组(但是这年头谁会不用List拿着原始数组去clone...)

扩展阅读:
关于第13条的访谈