如何正确覆盖克隆方法?

Posted

技术标签:

【中文标题】如何正确覆盖克隆方法?【英文标题】:How to properly override clone method? 【发布时间】:2011-01-20 13:41:14 【问题描述】:

我需要在我的一个没有超类的对象中实现深度克隆。

处理超类(即Object)抛出的已检查CloneNotSupportedException 的最佳方法是什么?

一位同事建议我按以下方式处理:

@Override
public MyObject clone()

    MyObject foo;
    try
    
        foo = (MyObject) super.clone();
    
    catch (CloneNotSupportedException e)
    
        throw new Error();
    

    // Deep clone member fields here

    return foo;

这对我来说似乎是一个很好的解决方案,但我想把它扔给 *** 社区,看看是否还有其他可以包含的见解。谢谢!

【问题讨论】:

如果你知道父类实现了Cloneable,那么抛出一个AssertionError 而不仅仅是一个普通的Error 更有表现力。 编辑:我宁愿不使用 clone(),但项目已经基于它,此时不值得重构所有引用。 最好现在咬紧牙关而不是以后(好吧,除非您即将投入生产)。 我们距离完成还有一个半月的时间。我们无法证明时间的合理性。 我认为这个解决方案是完美的。并且不要对使用克隆感到难过。如果您有一个用作传输对象的类,并且它只包含原始数据和不可变数据,如 String 和 Enums,那么由于教条的原因,除了 clone 之外的所有内容都将完全浪费时间。请记住克隆做什么和不做什么! (没有深度克隆) 【参考方案1】:

你一定要使用clone吗?大多数人都同意 Java 的 clone 已损坏。

Josh Bloch on Design - Copy Constructor versus Cloning

如果你读过我书中关于克隆的内容,尤其是如果你读到字里行间,你就会知道我认为clone 被深深地破坏了。 [...] 很遗憾 Cloneable 坏了,但它确实发生了。

您可以在他的书Effective Java 2nd Edition, Item 11: Override clone judiciously 中阅读更多关于该主题的讨论。他建议改用复制构造函数或复制工厂。

他接着写了几页关于如果你觉得必须的话,你应该如何实现clone。但他以这个结尾:

所有这些复杂性真的有必要吗?很少。如果你扩展一个实现Cloneable 的类,你别无选择,只能实现一个行为良好的clone 方法。否则,最好提供对象复制的替代方法,或者干脆不提供该功能

重点是他的,不是我的。


既然您明确表示您别无选择,只能实施 clone,在这种情况下您可以执行以下操作:确保 MyObject extends java.lang.Object implements java.lang.Cloneable。如果是这种情况,那么您可以保证您将永远捕获CloneNotSupportedException。正如一些人所建议的那样,抛出 AssertionError 似乎是合理的,但您也可以添加注释来解释为什么永远不会输入 catch 块在这种特殊情况下


或者,正如其他人也建议的那样,您也许可以在不调用 super.clone 的情况下实现 clone

【讨论】:

不幸的是,该项目已经使用克隆方法编写,否则我绝对不会使用它。我完全同意你的观点,Java 的克隆实现是 fakakta。 如果一个类及其所有超类在其克隆方法中调用super.clone(),则子类通常只需在添加内容需要克隆的新字段时覆盖clone()。如果任何超类使用new 而不是super.clone(),则所有子类都必须覆盖clone(),无论它们是否添加任何新字段。【参考方案2】:

有时实现复制构造函数更简单:

public MyObject (MyObject toClone) 

它为您省去了处理CloneNotSupportedException 的麻烦,与final 字段一起使用,您不必担心返回的类型。

【讨论】:

【参考方案3】:

您的代码的工作方式非常接近于“规范”的编写方式。不过,我会在捕获中抛出一个AssertionError。它表示永远不应该到达那条线。

catch (CloneNotSupportedException e) 
    throw new AssertionError(e);

【讨论】:

查看@polygenelubricants 的回答,了解为什么这可能是个坏主意。 @KarlRichter 我已经阅读了他们的答案。它并没有说这是一个坏主意,除了 Cloneable 通常是一个坏主意,正如有效 Java 中所解释的那样。 OP 已经表示他们必须使用Cloneable。所以我不知道我的答案还有什么可以改进的,除了可能直接删除它。【参考方案4】:

有两种情况会抛出CloneNotSupportedException

    被克隆的类没有实现Cloneable(假设实际的克隆最终还是遵循Object的克隆方法)。如果您在其中编写此方法的类实现了Cloneable,则永远不会发生这种情况(因为任何子类都会适当地继承它)。 异常是由实现显式抛出的 - 当超类为 Cloneable 时,这是防止子类中可克隆性的推荐方法。

后一种情况不能发生在您的类中(因为您直接在 try 块中调用超类的方法,即使是从调用 super.clone() 的子类中调用),前一种情况不应发生,因为您的类显然应该实现Cloneable

基本上,您应该肯定地记录错误,但在这种特殊情况下,只有在您弄乱了类的定义时才会发生这种情况。因此将其视为NullPointerException(或类似)的检查版本 - 如果您的代码正常运行,它将永远不会被抛出。


在其他情况下,您需要为这种可能性做好准备 - 无法保证给定对象可克隆的,因此在捕获异常时,您应该根据这种情况采取适当的措施(继续对于现有对象,采用另一种克隆策略,例如序列化-反序列化,如果您的方法需要可克隆的参数等,则抛出IllegalParameterException

编辑:虽然总的来说我应该指出,是的,clone() 确实很难正确实现,而且调用者很难知道返回值是否是他们想要的,当你考虑深克隆与浅克隆。完全避免整个事情并使用另一种机制通常会更好。

【讨论】:

如果一个对象公开了一个公共克隆方法,任何不支持它的派生对象都会违反里氏替换原则。如果克隆方法受到保护,我认为最好用其他方法而不是返回正确类型的方法来隐藏它,以防止子类甚至尝试调用super.clone()【参考方案5】:

使用serialization 进行深层复制。这不是最快的解决方案,但它不依赖于类型。

【讨论】:

考虑到我已经在使用 GSON,这是一个很棒且显而易见的解决方案。【参考方案6】:

您可以像这样实现受保护的复制构造函数:

/* This is a protected copy constructor for exclusive use by .clone() */
protected MyObject(MyObject that) 
    this.myFirstMember = that.getMyFirstMember(); //To clone primitive data
    this.mySecondMember = that.getMySecondMember().clone(); //To clone complex objects
    // etc


public MyObject clone() 
    return new MyObject(this);

【讨论】:

这在大多数情况下都不起作用。你不能对getMySecondMember()返回的对象调用clone(),除非它有public clone方法。 显然,您需要在希望能够克隆的每个对象上实现此模式,或者找到另一种方法来深度克隆每个成员。 是的,这是必要的要求。【参考方案7】:

尽管这里的大多数答案都是有效的,但我需要告诉您,您的解决方案也是实际的 Java API 开发人员如何做到的。 (Josh Bloch 或 Neal Gafter)

这里是 openJDK ArrayList 类的摘录:

public Object clone() 
    try 
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
     catch (CloneNotSupportedException e) 
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    

正如您已经注意到和其他人提到的,如果您声明实现了Cloneable 接口,CloneNotSupportedException 几乎没有机会被抛出。

另外,如果您在被覆盖的方法中没有做任何新的事情,则不需要覆盖该方法。仅当需要对对象进行额外操作或需要将其公开时才需要覆盖它。

最终,最好还是避免它并使用其他方式。

【讨论】:

【参考方案8】:
public class MyObject implements Cloneable, Serializable   

    @Override
    @SuppressWarnings(value = "unchecked")
    protected MyObject clone()
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try 
            ByteArrayOutputStream bOs = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bOs);
            oos.writeObject(this);
            ois = new ObjectInputStream(new ByteArrayInputStream(bOs.toByteArray()));
            return  (MyObject)ois.readObject();

         catch (Exception e) 
            //Some seriouse error :< //
            return null;
        finally 
            if (oos != null)
                try 
                    oos.close();
                 catch (IOException e) 

                
            if (ois != null)
                try 
                    ois.close();
                 catch (IOException e) 

                
        
    

【讨论】:

所以你刚刚将它写入文件系统并读回了对象。好的,这是处理克隆的最佳方法吗? SO社区的任何人都可以评论这种方法吗?我想这不必要地将克隆和序列化 - 两个完全不同的概念联系起来。我会等着看其他人对此有什么看法。 它没有进入文件系统,它在主内存中(ByteArrayOutputStream)。对于深度嵌套的对象,它的解决方案效果很好。特别是如果您不经常需要它,例如在单元测试中。其中性能不是主要目标。【参考方案9】:

仅仅因为 java 的 Cloneable 实现被破坏,并不意味着您不能创建自己的实现。

如果 OP 的真正目的是创建一个深度克隆,我认为可以创建这样的接口:

public interface Cloneable<T> 
    public T getClone();

然后使用前面提到的原型构造函数来实现它:

public class AClass implements Cloneable<AClass> 
    private int value;
    public AClass(int value) 
        this.vaue = value;
    

    protected AClass(AClass p) 
        this(p.getValue());
    

    public int getValue() 
        return value;
    

    public AClass getClone() 
         return new AClass(this);
    

和另一个具有 AClass 对象字段的类:

public class BClass implements Cloneable<BClass> 
    private int value;
    private AClass a;

    public BClass(int value, AClass a) 
         this.value = value;
         this.a = a;
    

    protected BClass(BClass p) 
        this(p.getValue(), p.getA().getClone());
    

    public int getValue() 
        return value;
    

    public AClass getA() 
        return a;
    

    public BClass getClone() 
         return new BClass(this);
    

通过这种方式,您可以轻松地深度克隆 BClass 类的对象,而无需 @SuppressWarnings 或其他花哨的代码。

【讨论】:

以上是关于如何正确覆盖克隆方法?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确克隆二维数组?

如何正确克隆(jQuery)通过 PIE 应用样式的元素?

实现 Cloneable 接口如何允许克隆对象,因为它是一个标记接口并且没有任何方法? [复制]

如何防止(覆盖或格式化或克隆其他任何内容)我的 NFC TAG?

克隆 CVPixelBuffer - 如何?

如何正确克隆 JavaScript 对象?