clone()vs复制构造函数vs工厂方法?


81

我对使用Java实现clone()进行了快速的搜索,发现:http : //www.javapractices.com/topic/TopicAction.do?Id=71

它具有以下注释:

复制构造函数和静态工厂方法提供了克隆的替代方法,并且易于实现。

我要做的只是制作一个深层副本。实现clone()似乎很有意义,但是这篇在Google上排名很高的文章使我有些害怕。

这是我注意到的问题:

复制构造函数不适用于泛型。

这是一些不会编译的伪代码。

public class MyClass<T>{
   ..
   public void copyData(T data){
       T copy=new T(data);//This isn't going to work.    
   }
   ..
}

示例1:在泛型类中使用复制构造函数。

工厂方法没有标准名称。

有一个可重用代码的接口是非常好的。

public class MyClass<T>{
    ..
    public void copyData(T data){
        T copy=data.clone();//Throws an exception if the input was not cloneable
    }
    ..
}

示例2:在泛型类中使用clone()。

我注意到克隆不是静态方法,但是否仍需要对所有受保护字段进行深拷贝?在实现clone()时,在非克隆子类中引发异常的额外工作对我而言似乎微不足道。

我想念什么吗?任何见解将不胜感激。


Answers:


52

克隆基本上是坏的。使用泛型没有什么容易的。如果您有这样的事情(简称以简化理解):

public class SomeClass<T extends Copyable> {


    public T copy(T object) {
        return (T) object.copy();
    }
}

interface Copyable {
    Copyable copy();
}

然后通过编译器警告您可以完成工作。因为泛型在运行时被擦除,所以执行复制的操作将在其中生成编译器警告,提示生成强制转换。在这种情况下,这是不可避免的。。在某些情况下(避免使用kb304),这是可以避免的,但在所有情况下都不能避免。考虑一下您必须支持实现该接口的子类或未知类的情况(例如,您遍历不一定生成相同类的可复制对象的集合)。


2
也可以在没有编译器警告的情况下进行。
akarnokd

@ kd304,如果您的意思是禁止编译器警告,是的,但实际上没有什么不同。如果您的意思是完全可以避免发出警告,请详细说明。
Yishai

@Yishai:看看我的回答。我指的不是抑制警告。
akarnokd

拒绝投票是因为Copyable<T>在kd304的答案中使用起来更有意义。
西蒙·佛斯伯格

25

还有Builder模式。有关详细信息,请参见有效的Java。

我不理解您的评价。在复制构造函数中,您完全了解类型,为什么需要使用泛型?

public class C {
   public int value;
   public C() { }
   public C(C other) {
     value = other.value;
   }
}

最近在这里有一个类似的问题。

public class G<T> {
   public T value;
   public G() { }
   public G(G<? extends T> other) {
     value = other.value;
   }
}

可运行的示例:

public class GenTest {
    public interface Copyable<T> {
        T copy();
    }
    public static <T extends Copyable<T>> T copy(T object) {
        return object.copy();
    }
    public static class G<T> implements Copyable<G<T>> {
        public T value;
        public G() {
        }
        public G(G<? extends T> other) {
            value = other.value;
        }
        @Override
        public G<T> copy() {
            return new G<T>(this);
        }
    }
    public static void main(String[] args) {
        G<Integer> g = new G<Integer>();
        g.value = 1;
        G<Integer> f = g.copy();
        g.value = 2;
        G<Integer> h = copy(g);
        g.value = 3;
        System.out.printf("f: %s%n", f.value);
        System.out.printf("g: %s%n", g.value);
        System.out.printf("h: %s%n", h.value);
    }
}

1
+1这是实现复制构造函数的最简单但有效的方法
dfa

请注意,上面的MyClass是通用的,StackOverflow吞下了<T>。
User1

修正了您的问题格式。在每行上使用四个空格,而不要使用pre + code标记。
akarnokd

我在G的复制方法上遇到错误-“必须重写超类方法”
Carl Pritchett 2012年

3
(我知道这很旧了,但是为了后代)@CarlPritchett:这个错误将在Java 1.5及以下版本中显示。从Java 1.6开始,允许将接口方法标记为@Override。
Carrotman42年

10

Java没有与C ++相同的复制构造函数。

您可以有一个构造函数,该构造函数将相同类型的对象用作参数,但是很少有类支持此构造函数。(少于支持克隆能力的数量)

对于通用克隆,我有一个辅助方法,该方法创建一个类的新实例,并使用反射(实际上类似于反射,但速度更快)从原始副本(浅副本)复制字段。

对于深层副本,一种简单的方法是序列化对象并将其反序列化。

顺便说一句:我的建议是使用不可变对象,那么您无需克隆它们。;)


您能解释一下为什么我使用不可变对象时不需要克隆吗?在我的应用程序中,我需要另一个对象,该对象的数据与现有对象的数据完全相同。所以我需要以某种方式复制它
振亚

2
@Ievgen如果您需要两个引用相同的数据,则可以复制该引用。您只需要复制可能改变的对象的内容即可(但是对于不可变的对象,您不会知道)
Peter Lawrey

可能我只是不了解不可变对象的好处。假设我有一个JPA /休眠实体,我需要基于现有实体创建另一个JPA实体,但是我需要更改新实体的ID。如何处理不可变的对象?
振亚

根据定义,@ Ievgen您不能更改不可变对象。 String例如是一个不可变的对象,您可以传递字符串,而不必复制它。
彼得·劳里

6

我认为Yishai的答案可能会有所改善,因此我们可以使用以下代码进行警告:

public class SomeClass<T extends Copyable<T>> {

    public T copy(T object) {
        return object.copy();
    }
}

interface Copyable<T> {
    T copy();
}

这样,需要实现Copyable接口的类必须像这样:

public class MyClass implements Copyable<MyClass> {

    @Override
    public MyClass copy() {
        // copy implementation
        ...
    }

}

2
几乎。Copyable接口应声明为:interface Copyable<T extends Copyable>,这是最接近可以在Java中编码的自类型的东西。
递归

当然是你的意思interface Copyable<T extends Copyable<T>>吧?;)
Adowrath

3

以下是一些不利于许多开发人员的缺点 Object.clone()

  1. 使用Object.clone()方法需要我们在代码中添加很多语法,例如实现Cloneable接口,定义clone()方法和句柄CloneNotSupportedException,最后调用Object.clone()并将其转换为对象。
  2. Cloneable接口缺少clone()方法,它是一个标记器接口,并且其中没有任何方法,但是我们仍然需要实现它,只是告诉JVM我们可以clone()在对象上执行。
  3. Object.clone()受保护,因此我们必须提供自己的clone()间接调用Object.clone()
  4. 我们没有控制对象的构造,因为Object.clone()它不会调用任何构造函数。
  5. 例如,如果我们clone()在子类中编写方法,Person则其所有超类都应clone()在其中定义方法或从另一个父类继承该方法,否则super.clone()链将失败。
  6. Object.clone()仅支持浅拷贝,因此我们新克隆的对象的引用字段仍将保留原始对象的字段所保存的对象。为了克服这个问题,我们需要clone()在每个引用类的类中实现,然后clone()像下面的示例一样在我们的方法中分别克隆它们。
  7. 我们不能操纵final字段,Object.clone()因为final字段只能通过构造函数进行更改。在我们的例子中,如果我们希望每个Person对象的ID都是唯一的,那么如果使用ID,我们将得到重复的对象,Object.clone()因为Object.clone()它将不会调用构造函数,并且finalid字段也不能从修改Person.clone()

复制构造函数比Object.clone()它们更好,因为

  1. 不要强迫我们实现任何接口或抛出任何异常,但是如果需要的话,我们当然可以做到。
  2. 不需要任何强制转换。
  3. 不需要我们依赖未知的对象创建机制。
  4. 不需要家长班遵守任何合同或执行任何事情。
  5. 让我们修改最终字段。
  6. 允许我们完全控制对象创建,可以在其中编写初始化逻辑。

阅读有关Java克隆的更多信息-复制构造函数与克隆


1

通常,clone()与受保护的副本构造函数协同工作。这样做是因为clone()与构造函数不同,可以是虚拟的。

在派生自超一流基地的班级机构中,我们有

class Derived extends Base {
}

因此,最简单的做法是,将带有clone()的虚拟副本构造函数添加到其中。(在C ++中,Joshi建议将clone作为虚拟副本构造函数。)

protected Derived() {
    super();
}

protected Object clone() throws CloneNotSupportedException {
    return new Derived();
}

如果您想按建议的方式调用super.clone(),并且必须将这些成员添加到类中,它将变得更加复杂。

final String name;
Address address;

/// This protected copy constructor - only constructs the object from super-class and
/// sets the final in the object for the derived class.
protected Derived(Base base, String name) {
   super(base);
   this.name = name;
}

protected Object clone() throws CloneNotSupportedException {
    Derived that = new Derived(super.clone(), this.name);
    that.address = (Address) this.address.clone();
}

现在,如果执行死刑,

Base base = (Base) new Derived("name");

然后你做了

Base clone = (Base) base.clone();

这将调用Derived类(上面的类)中的clone(),这将调用super.clone()-可能会或可能不会实现,但建议您调用它。然后,该实现将super.clone()的输出传递给受保护的副本构造函数,该构造函数采用Base,然后将所有最终成员传递给它。

然后,该副本构造函数调用超类的副本构造函数(如果您知道它具有一个),并设置最终值。

当您回到clone()方法时,您将设置所有非最终成员。

精明的读者会注意到,如果在Base中有一个复制构造函数,它将由super.clone()调用-并在受保护的构造函数中调用超级构造函数时再次调用它,因此您可能正在调用超级复制构造函数两次。希望,如果它锁定资源,它将知道这一点。


0

可能适用于您的一种模式是Bean级复制。基本上,您使用no-arg构造函数并调用各种setter以提供数据。您甚至可以使用各种bean属性库来相对容易地设置属性。这与执行clone()不同,但出于许多实际目的,这很好。


0

从某种意义上讲,Cloneable接口是无效的,但克隆工作得很好,可以为8个字段及更多的大对象带来更好的性能,但是它将使转义分析失败。因此最好在大多数情况下使用复制构造函数。在数组上使用clone的速度比Arrays.copyOf快,因为可以保证长度相同。

此处有更多详细信息https://arnaudroger.github.io/blog/2017/07/17/deep-dive-clone-vs-copy.html


0

如果不是100%意识到的所有怪癖clone(),那么我建议您不要使用它。我不会说clone()坏了。我会说:只有在完全确定这是您的最佳选择时才使用它。复制构造函数(或工厂方法,我认为并不重要)很容易编写(可能很长,但很容易),它仅复制要复制的内容,并复制要复制的内容的方式。您可以根据自己的实际需求进行调整。

另外:调用复制构造函数/工厂方法时,调试发生的事情很容易。

并且clone()不创建现成的对象的“深层”副本,假设您的意思是不仅Collection复制了引用(例如)。但是请在此处阅读有关深层和浅层的更多信息: 深层副本,浅层副本,克隆


-2

您所缺少的是,克隆默认情况下会创建浅表副本,而按照惯例,创建深副本通常是不可行的。

问题在于,如果无法跟踪已访问了哪些对象,就无法真正创建循环对象图的深层副本。clone()不提供此类跟踪(因为它必须是.clone()的参数),因此仅创建浅表副本。

即使您自己的对象对其所有成员都调用.clone,它仍然不会是深层副本。


4
对任何任意对象进行深度复制clone()可能是不可行的,但实际上,对于许多对象层次结构而言,这是可管理的。它仅取决于您拥有的对象类型及其成员变量。
Shiny先生和新安宇

模棱两可比许多人声称的要少得多。如果有一个可SuperDuperList<T>克隆或可克隆的衍生物,则对其进行克隆应产生一个与被克隆的相同类型的新实例。它应与原始文件分离,但应以与T原始文件相同的顺序引用相同的。从那一点开始,对任何一个列表进行的任何操作都不会影响存储在另一个对象中的对象的标识。我知道没有有用的“约定”来要求通用集合表现出任何其他行为。
2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.