【问题标题】:How does Cloneable work in Java and how do I use it?Cloneable 在 Java 中如何工作以及如何使用它?
【发布时间】:2011-05-04 04:04:44
【问题描述】:

我想知道以下内容:

  1. Cloneable 表示我们可以拥有对象的克隆或副本,通过 实现Cloneable 接口。有什么优点和 这样做有什么坏处?
  2. 如果对象是 复合对象?

【问题讨论】:

    标签: java cloneable


    【解决方案1】:

    关于Cloneable,您首先应该知道的是 - 不要使用它。

    Cloneable实现克隆是非常困难的,不值得。

    使用其他选项代替,例如 apache-commons SerializationUtils(深度克隆)或 BeanUtils(浅克隆),或者简单地使用复制构造函数。

    See here 了解 Josh Bloch 关于使用Cloneable 进行克隆的观点,这解释了该方法的许多缺点。 (Joshua Bloch 是 Sun 的员工,领导了众多 Java 功能的开发。)

    【讨论】:

    • 我链接了布洛赫的话(而不是引用它们)
    • 请注意,Block 表示不要使用 Cloneable。他没有说不要使用克隆(或者至少我希望不要)。有很多方法可以简单地实现克隆,它们比使用反射的 SerializationUtils 或 BeanUtils 等类更有效。请参阅下面的我的帖子以获取示例。
    • 定义接口时复制构造函数的替代方法是什么?只需添加一个复制方法?
    • @benez 我会说是的。由于 java-8 你可以在接口中有static 方法,所以只需提供一个static WhatEverTheInterface copy(WhatEverTheInterface initial)?但我想知道这会给您带来什么,因为您在克隆时从对象复制字段,但接口仅定义方法。需要解释一下吗?
    • 很抱歉,我将对此投反对票,这是一个错误的建议。 SerializationUtils 很昂贵,并且在他们自己的 java 文档中有很好的记录,更进一步的问题是如何使用可克隆的,应该给出一个正确的例子而不是不正确的建议。我同意可以添加免责声明,但不要使用可克隆是完全错误的答案。
    【解决方案2】:
    1. 克隆调用了一种额外的语言构造对象的方式 - 没有构造函数。
    2. 克隆要求您以某种方式使用 CloneNotSupportedException 进行处理 - 或者打扰客户端代码进行处理。
    3. 好处很小 - 您不必手动编写复制构造函数。

    因此,请谨慎使用 Cloneable。与您为做好每一件事所付出的努力相比,它并没有给您带来足够的好处。

    【讨论】:

    • 正如 Bozho 所说,不要使用 Cloneable。相反,使用复制构造函数。 javapractices.com/topic/TopicAction.do?Id=12
    • @Bane,如果你不知道要克隆的对象的类型,你怎么知道要调用哪个类的复制构造函数?
    • @Steve:我不关注。如果你要克隆一个对象,我想你已经知道它是什么类型了——毕竟,你手头有你计划克隆的对象。而且,如果在某种情况下,您的对象已将其特定类型丢失为更通用的类型,您难道不能使用简单的“实例”来评估它吗???
    • @Bane:假设您有一个对象列表,这些对象都是从类型 A 派生的,可能有 10 种不同的类型。你不知道每个对象的类型是什么。在这种情况下使用 instanceof 是一个非常糟糕的主意。如果添加另一种类型,则每次执行此操作时都必须添加另一个 instanceof 测试。而且,如果派生类在另一个您甚至无法访问的包中怎么办?克隆是一种常见的模式。是的,java 实现很糟糕,但是有很多方法可以很好地解决它。复制构造函数不是等效操作。
    • @Charles:由于没有详细的示例,并且缺乏处理此类问题的最新经验,我将不得不听从 Bloch 的意见。项目#11。它很长而且有点难读,但它基本上说“尽可能避免克隆,复制构造函数是你的朋友”。
    【解决方案3】:

    A) 与复制构造函数相比,克隆并没有太多优势。最大的可能是创建完全相同动态类型的新对象的能力(假设声明的类型是可克隆的并且具有公共克隆方法)。

    B) 默认克隆创建一个浅拷贝,除非你的克隆实现改变了它,否则它将保持一个浅拷贝。这可能很困难,尤其是如果您的课程有最终字段

    Bozho 是对的,克隆很难做到正确。复制构造函数/工厂将满足大多数需求。

    【讨论】:

      【解决方案4】:

      不幸的是,Cloneable 本身只是一个标记接口,也就是说:它没有定义 clone() 方法。

      所做的是更改受保护的 Object.clone() 方法的行为,该方法将为未实现 Cloneable 的类抛出 CloneNotSupportedException,并为实现的类执行成员级浅拷贝。

      即使这是您正在寻找的行为,您仍然需要实现自己的 clone() 方法才能将其公开。

      当实现你自己的clone()时,想法是从super.clone()创建的对象开始,它保证是正确的类,然后做任何额外的字段填充以防浅拷贝不是你想要的。从 clone() 调用构造函数会有问题,因为如果子类想要添加自己的额外可克隆逻辑,这会破坏继承;如果要调用 super.clone() 在这种情况下会得到一个错误类的对象。

      这种方法会绕过任何可能在您的构造函数中定义的逻辑,这可能会出现问题。

      另一个问题是任何忘记覆盖 clone() 的子类将自动继承默认的浅拷贝,这可能不是你想要的可变状态(现在将在源和副本之间共享)。

      出于这些原因,大多数开发人员不使用 Cloneable,而是简单地实现一个复制构造函数。

      有关 Cloneable 的更多信息和潜在陷阱,我强烈推荐 Joshua Bloch 的《Effective Java》一书

      【讨论】:

        【解决方案5】:

        克隆是一种基本的编程范式。 Java 可能在很多方面实现得很糟糕,这一事实并没有减少对克隆的需求。而且,很容易实现克隆,无论你希望它如何工作,浅的、深的、混合的,等等。如果你愿意,你甚至可以为函数使用名称 clone 而不是实现 Cloneable。

        假设我有 A、B 和 C 类,其中 B 和 C 派生自 A。如果我有一个 A 类型的对象列表,如下所示:

        ArrayList<A> list1;
        

        现在,该列表可以包含 A、B 或 C 类型的对象。您不知道这些对象是什么类型。所以,你不能像这样复制列表:

        ArrayList<A> list2 = new ArrayList<A>();
        for(A a : list1) {
            list2.add(new A(a));
        }
        

        如果对象实际上是 B 或 C 类型,您将不会得到正确的副本。而且,如果 A 是抽象的怎么办?现在,有人提出了这个建议:

        ArrayList<A> list2 = new ArrayList<A>();
        for(A a : list1) {
            if(a instanceof A) {
                list2.add(new A(a));
            } else if(a instanceof B) {
                list2.add(new B(a));
            } else if(a instanceof C) {
                list2.add(new C(a));
            }
        }
        

        这是一个非常非常糟糕的主意。如果添加新的派生类型怎么办?如果 B 或 C 在另一个包中并且您在此类中无权访问它们怎么办?

        你想做的是:

        ArrayList<A> list2 = new ArrayList<A>();
        for(A a : list1) {
            list2.add(a.clone());
        }
        

        很多人已经指出为什么克隆的基本 Java 实现存在问题。但是,通过这种方式很容易克服:

        A类:

        public A clone() {
            return new A(this);
        }
        

        B 类:

        @Override
        public B clone() {
            return new B(this);
        }
        

        在 C 类中:

        @Override
        public C clone() {
            return new C(this):
        }
        

        我没有实现 Cloneable,只是使用相同的函数名。如果你不喜欢它,就给它起个名字吧。

        【讨论】:

        • 我在单独回答您的评论后才看到这个;我知道您现在要去哪里,但是有两件事:1)OP专门询问了有关使用Cloneable(而不是关于克隆的通用概念)的问题,以及2)您在尝试区分复制构造函数时有点费解和克隆的一般概念。您在此处传达的想法是有效的,但从根本上讲,您只是在使用复制构造函数。 ;)
        • 虽然我确实想说我同意你的方法,包括 A#copyMethod(),而不是强迫用户直接调用复制构造函数。
        • 我赞成这一点,因为它准确地为所提问题提供了示例。投票最多的答案并不总是正确的建议。 SerializationUtils 对性能和类似的 BeanUtils 非常不利。复制构造函数是双赢的,但是如果实施得当,克隆本身也不错。
        【解决方案6】:

        Cloneable 的缺点是什么?

        如果要复制的对象具有组合,则克隆非常危险。在这种情况下,您需要考虑以下可能的副作用,因为克隆会创建浅拷贝:

        假设您有一个对象来处理与数据库相关的操作。比如说,该对象具有Connection 对象作为属性之一。

        所以当有人创建originalObject 的克隆时,正在创建的对象,比如说cloneObject。 这里originalObjectcloneObjectConnection 对象持有相同的引用。

        假设originalObject 关闭了Connection 对象,那么现在cloneObject 将无法工作,因为connection 对象在它们之间共享并且实际上被originalObject 关闭。

        如果假设您要克隆一个具有 IOStream 作为属性的对象,可能会出现类似的问题。

        如果对象是复合对象,递归克隆如何发生?

        Cloneable 执行浅拷贝。意思是原始对象和克隆对象的数据将指向相同的引用/内存。 相反,深拷贝的情况下,将原始对象内存中的数据复制到克隆对象的内存中。

        【讨论】:

        • 你的最后一段很混乱。 Cloneable 不执行复制,Object.clone 执行。 “将原始对象内存中的数据复制到克隆对象的内存”正是Object.clone 所做的。您需要谈论引用对象的内存来描述深度复制。
        猜你喜欢
        • 1970-01-01
        • 2015-05-28
        • 2012-01-22
        • 2011-10-27
        • 2013-03-25
        • 2016-07-24
        • 2015-04-04
        相关资源
        最近更新 更多