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

Posted

技术标签:

【中文标题】克隆() vs 复制构造函数 vs 工厂方法?【英文标题】:clone() vs copy constructor vs factory method? 【发布时间】:2010-11-09 12:25:28 【问题描述】:

我在 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 不是一个静态方法,但是是否仍然需要对所有受保护的字段进行深拷贝?在实现 clone() 时,在不可克隆子类中引发异常的额外工作对我来说似乎微不足道。

我错过了什么吗?任何见解将不胜感激。

【问题讨论】:

另见***.com/questions/1086929/… 【参考方案1】:

基本上,clone is broken。没有什么可以轻松地使用泛型。如果你有这样的事情(缩短以了解重点):

public class SomeClass<T extends Copyable> 


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


interface Copyable 
    Copyable copy();

然后通过编译器警告,您可以完成工作。因为泛型在运行时被擦除,所以复制的东西会在其中产生编译器警告。 在这种情况下是无法避免的。。在某些情况下是可以避免的(感谢 kb304),但并非完全如此。考虑您必须支持实现接口的子类或未知类的情况(例如,您正在遍历不一定生成相同类的可复制集合)。

【讨论】:

也可以做到没有编译器警告。 @kd304,如果您的意思是抑制编译器警告,那么是的,但实际上并没有什么不同。如果您的意思是可以完全避免警告,请详细说明。 @Yishai:看我的回答。我指的不是抑制警告。 拒绝投票,因为 kd304 的答案中的Copyable&lt;T&gt; 使用起来更有意义。【参考方案2】:

还有建造者模式。有关详细信息,请参阅有效的 Java。

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

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

最近有一个类似的问题here。

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 这是实现复制构造函数的最简单但有效的方法 注意上面的MyClass是泛型的,***吞下了 修正了你的问题格式。每行使用四个空格,而不是 pre+code 标记。 G 的复制方法出错 - “必须重写超类方法” (我知道这是旧的,但为了后代)@CarlPritchett:此错误将出现在 Java 1.5 及更低版本中。从 Java 1.6 开始允许将接口方法标记为 @Override。【参考方案3】:

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

你可以有一个构造函数,它接受一个相同类型的对象作为参数,但是很少有类支持这个。 (小于支持克隆的数量)

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

对于深拷贝,一个简单的方法是序列化对象并反序列化它。

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

【讨论】:

您能否解释一下,如果我使用不可变对象,为什么我不需要克隆?在我的应用程序中,我需要另一个与现有对象具有完全相同数据的对象。所以我需要以某种方式复制它 @Ievgen 如果需要两个引用相同的数据,可以复制引用。你只需要复制一个对象的内容,如果它可能会改变(但对于不可变的对象,你知道它不会) 可能我只是不明白不可变对象的好处。假设我有一个 JPA/hibernate 实体,我需要在现有的基础上创建另一个 JPA 实体,但我需要更改新实体的 id。我将如何处理不可变对象? @Ievgen 根据定义,您不能更改不可变对象。例如,String 是一个不可变对象,您可以传递一个字符串而无需复制它。【参考方案4】:

我认为 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
        ...
    


【讨论】:

差不多。 Copyable 接口应声明为:interface Copyable&lt;T extends Copyable&gt;,这是最接近您可以在 Java 中编码的 self 类型的东西。 当然你的意思是interface Copyable&lt;T extends Copyable&lt;T&gt;&gt;,对吧? ;)【参考方案5】:

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

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

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

    不要强迫我们实现任何接口或抛出任何异常,但如果需要,我们一定可以做到。 不需要任何强制转换。 不要求我们依赖未知的对象创建机制。 不要求父类遵循任何契约或实现任何东西。 允许我们修改最终字段。 允许我们完全控制对象的创建,我们可以在其中编写初始化逻辑。

阅读更多Java Cloning - Copy Constructor versus Cloning

【讨论】:

【参考方案6】:

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

在 Derived from a super class Base 的类体中,我们有

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 并将任何最终成员传递给它。

然后那个拷贝构造函数调用超类的拷贝构造函数(如果你知道它有一个),并设置final。

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

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

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

Cloneable 接口已损坏,从某种意义上说它无用,但 clone 效果很好,并且可以为大对象带来更好的性能 - 8 个字段或更多,但它会导致逃逸分析失败。所以大多数时候最好使用复制构造函数。 在数组上使用克隆比 Arrays.copyOf 更快,因为长度保证是相同的。

更多详情在这里https://arnaudroger.github.io/blog/2017/07/17/deep-dive-clone-vs-copy.html

【讨论】:

【参考方案9】:

如果一个人不是 100% 了解clone() 的所有怪癖,那么我建议远离它。我不会说clone() 坏了。我想说:只有在您完全确定它是您的最佳选择时才使用它。 复制构造函数(或工厂方法,我认为这并不重要)很容易编写(可能很长,但很容易),它只复制您想要复制的内容,并以您想要复制的方式复制。您可以根据自己的具体需求对其进行修剪。

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

并且clone() 不会创建开箱即用的对象的“深层”副本,假设您的意思是不仅复制了引用(例如对Collection)。但是在这里阅读更多关于深浅的信息: Deep copy, shallow copy, clone

【讨论】:

【参考方案10】:

您缺少的是克隆默认和约定创建浅拷贝,而使其创建深拷贝通常是不可行的。

问题在于,您无法真正创建循环对象图的深层副本,而无法跟踪访问过的对象。 clone() 不提供这种跟踪(因为它必须是 .clone() 的参数),因此只会创建浅拷贝。

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

【讨论】:

对任意对象进行深度复制 clone() 可能是不可行的,但实际上这对于许多对象层次结构是可以管理的。这仅取决于您拥有什么样的对象以及它们的成员变量是什么。 歧义远比许多人声称的要少。如果一个人有一个可克隆的SuperDuperList&lt;T&gt; 或其衍生物,那么克隆它应该会产生一个与被克隆的实例类型相同的新实例;它应该与原件分离,但应以与原件相同的顺序引用相同的T。从那时起,对任一列表所做的任何操作都不会影响存储在另一个列表中的对象的identities。我知道没有有用的“约定”会要求通用集合展示任何其他行为。

以上是关于克隆() vs 复制构造函数 vs 工厂方法?的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript中的工厂方法构造函数与class

javascript工厂函数(factory function)vs构造函数(constructor function)

工厂方法VS构造器

静态工厂方法VS构造器

克隆是不是比构造函数/工厂方法提供了性能改进?

设计模式 模式PK:工厂模式VS建造者模式