对象反序列化是在Java中实现Prototype模式的正确方法吗?

时间:2014-05-27 15:10:06

标签: java design-patterns serialization deserialization prototype-pattern

TL; DR

我可以使用Serializable接口,ObjectOutputStreamObjectInputStream类来使用Java序列化/反序列化,并且可能在实现{的类中添加readObjectwriteObject {1}}是否是原型模式的有效实现?

注意

这个问题不是来讨论使用复制构造函数是否优于序列化/反序列化。


我知道Prototype Pattern概念(来自维基百科,强调我的):

  

原型模式是软件开发中的创新设计模式。当要创建的对象类型由原型实例确定时使用它,该实例被克隆以生成新对象。此模式用于:

     
      
  • 避免客户端应用程序中的对象创建者的子类,就像抽象工厂模式那样。

  •   
  • 避免以标准方式创建新对象的固有成本(例如,使用'new'关键字),当它对于给定的应用程序来说过于昂贵时。

  •   

从这个Q / A:Examples of GoF Design Patterns in Java's core libraries,BalusC解释说,只有当类实现Object#clone接口(标记接口类似于{{1)时,Java中的原型模式才由Cloneable实现。序列化/反序列化对象)。使用这种方法的问题在博客文章/相关问答中注明:

所以,另一个选择是使用复制构造函数来克隆你的对象(DIY方式),但这无法实现我上面强调的文本的原型模式:

  

避免以标准方式创建新对象的固有成本(例如,使用'new'关键字)

AFAIK创建对象而不调用其构造函数的唯一方法是通过反序列化,如此问题的接受答案示例中所述:How are constructors called during serialization and deserialization?

所以,我只是询问是否通过Serializable使用对象反序列化(并知道你正在做什么,将必要的字段标记为Serializable并理解这个过程的所有含义)或类似的方法将是原型模式的正确实现。

注意:我不认为解组XML文档是这种模式的正确实现,因为它调用了类构造函数。也可能在解组JSON内容时也会发生这种情况。


人们会建议使用对象构造函数,在使用简单对象时我会介意这个选项。这个问题更倾向于深度复制复杂的对象,我可能有5个级别的对象要克隆。例如:

ObjectOutputStream

假设我想要/需要复制transient的实例。在这种情况下,我不认为复制构造函数会适用。在这种情况下,拥有大量复制构造函数的IMO似乎不是一个好的设计。

4 个答案:

答案 0 :(得分:13)

直接使用Java对象序列化并不是Prototype模式,但可以使用序列化来实现模式。

Prototype模式将复制的责任放在要复制的对象上。如果直接使用序列化,则客户端需要提供反序列化和序列化代码。如果您拥有或计划编写所有要复制的类,则可以轻松将责任移交给这些类:

  • 定义Prototype接口,扩展Serializable并添加实例方法copy
  • 使用静态方法PrototypeUtility定义具体类copy,该方法在一个位置实现序列化和反序列化
  • 定义实现AbstractPrototype的抽象类Prototype。将其copy方法委托给PrototypeUtility.copy

需要Prototype的类可以实现Prototype本身并使用PrototypeUtility来完成工作,也可以只扩展AbstractPrototype。通过这样做,它还宣称它是安全的Serializable

如果您不拥有要复制其实例的类,则无法完全遵循Prototype模式,因为您无法将复制的责任转移到这些类。但是,如果这些类实现Serializable,您仍然可以通过直接使用序列化来完成工作。

关于复制构造函数,这些是复制其知道类的Java对象的好方法,但是它们不符合Prototype模式的要求,客户端不应该知道它的对象实例的类正在复制。不知道实例类但希望使用其复制构造函数的客户端必须使用反射来查找构造函数,该构造函数的唯一参数与其所属的类具有相同的类。这很难看,客户端无法确定它找到的构造函数是一个复制构造函数。实现接口可以干净地解决这些问题。

维基百科评论Prototype模式避免了创建新对象的成本似乎误导了我。 (我在“四人帮”的描述中没有看到任何内容。)维基百科创建的对象的示例是一个对象,它列出了文本中单词的出现,当然这些对象的查找成本很高。但是设计程序是愚蠢的,因此获取WordOccurrences实例的唯一方法是实际分析文本,特别是如果您因某种原因需要复制该实例。只需给它一个构造函数,其中包含描述实例整个状态的参数,并将它们分配给它的字段或复制构造函数。

因此,除非您正在使用隐藏其合理构造函数的第三方库,否则请忘记该性能。原型的重点是

  • 它允许客户端在不知道其类的情况下复制对象实例,并且
  • 它在没有创建工厂层次结构的情况下实现了这个目标,因为用AbstractFactory模式实现了相同的目标。

答案 1 :(得分:1)

我对这部分要求感到困惑:

  

注意:我不认为解组XML文档是正确的   因为调用类构造函数,所以实现了这种模式。   也可能在解组JSON内容时也会发生这种情况。

我知道您可能不想实现复制构造函数,但是您将始终拥有常规构造函数。如果这个构造函数由库调用那么它有什么关系呢?此外,Java中的对象创建很便宜。我已经使用Jackson来编组/解组Java对象并取得了巨大的成功。它具有高性能,并且具有许多可能对您的案例非常有用的强大功能。您可以按如下方式实施深度复印机:

import com.fasterxml.jackson.databind.ObjectMapper;

public class MyCloner {

    private ObjectMapper cloner; // with getter and setter

    public <T> clone(T toClone){
        String stringCopy = mapper.writeValueAsString(toClone);
        T deepClone = mapper.readValue(stringCopy, toClone.getClass());
        return deepClone;
    }
}

请注意,Jackson将自动使用Beans(getter + setter对,no-arg构造函数)。对于破坏该模式的类,它需要额外的配置。关于此配置的一个好处是它不需要您编辑现有的类,因此您可以使用JSON进行克隆,而无需知道正在使用JSON的代码的任何其他部分。

我喜欢这种方法与序列化的另一个原因是它更人性化可调试(只需查看字符串以查看数据是什么)。此外,还有大量工具可供使用JSON:

  1. Online JSON formatter
  2. Veiw JSON as HTML based webpage
  3. 而Java序列化的工具并不是很好。

    此方法的一个缺点是,默认情况下,默认情况下,原始对象中的重复引用在复制的对象中将是唯一的。这是一个例子:

     public class CloneTest {
         public class MyObject { }
         public class MyObjectContainer {
    
             MyObject refA;
             MyObject refB;
    
             // Getters and Setters omitted
    
         }
    
         public static void runTest(){
             MyCloner cloner = new MyCloner();
             cloner.setCloner(new ObjectMapper());
             MyObjectContainer container = new MyObjectContainer();
             MyObject duplicateReference = new MyObject();
             MyObjectContainer.setRefA(duplicateReference);
             MyObjectContainer.setRefB(duplicateReference);
             MyObjectContainer cloned = cloner.clone(container);
             System.out.println(cloned.getRefA() == cloned.getRefB()); // Will print false
             System.out.println(container.getRefA() == container.getRefB()); // Will print true
         }
    
    }
    

    鉴于有几种方法可以解决这个问题,每种方法各有利弊,我认为没有一种“正确”的方式来实现Java中的原型模式。正确的方法在很大程度上取决于您自己编写的环境。如果您的构造函数执行繁重的计算(并且无法绕过它们),那么我认为您没有太多选择,只能使用反序列化。否则,我更喜欢JSON / XML方法。如果不允许使用外部库并且我可以修改我的bean,那么我将使用Dave的方法。

答案 2 :(得分:1)

你的问题非常有趣Luiggi(我之所以投票赞成,因为这个想法很棒),你不会说出你真正关心的事情。所以我会尝试回答我所知道的事情并让你选择你认为有争议的东西:

  • 优点:

    • 在内存使用方面,通过使用序列化可以获得非常好的内存消耗,因为它以二进制格式序列化对象(而不是像json或更糟的文本:xml)。您可能必须选择一种策略来保持您的对象&#34;模式&#34;只要你需要它就在记忆中,并坚持使用少用的第一个持久的&#34;策略,或者&#34;首先使用第一个持久的&#34;
    • 编码很直接。有一些规则需要尊重,但是你没有很多复杂的结构,这仍然是可维护的
    • 不需要外部库,这在具有严格安全/法律规则的机构中非常有优势(对程序中使用的每个库进行验证)
    • 如果您不需要在JVM的程序版本之间维护对象。您可以从每个JVM更新中获益,因为速度是java程序的真正关注点,并且它与io操作(JMX,内存读/写,nio等)非常相关。因此,很有可能新版本将优化io /内存使用/序列化算法,您会发现无需更改代码即可更快地进行写入/读取。
  • 缺点:

    • 如果更改树中的任何对象,则会丢失所有原型。序列化仅适用于相同的对象定义
    • 您需要对对象进行反序列化以查看其中的内容:而不是原型模式是自我记录的&#39;如果你从Spring / Guice配置文件中获取它。保存到磁盘的二进制对象非常不透明
    • 如果您计划进行可重用的库,那么您可以向库用户强加一个非常严格的模式(在每个对象上实现Serializable,或者对不可序列化的dield使用transient)。此外,编译器无法检查此约束,您必须运行程序以查看是否存在错误(如果树中的对象对于测试为空,则可能不会立即显示)。当然,我将它与其他原型技术进行比较(例如,Guice的主要特点是编译时检查,Spring最近也做了)

我认为这就是我现在想到的所有内容,如果突然出现任何新方面,我会添加评论:)

当然,与调用构造函数相比,我不知道将对象写为字节的速度有多快。答案应该是批量写/读测试

但这个问题值得思考。

答案 3 :(得分:-1)

有些情况下,使用复制构造函数创建新对象不同于以标准方式创建新对象&#34;&#34;。您的问题中的维基百科链接中解释了一个示例。在该示例中,要使用构造函数WordOccurrences(text,word)创建新的WordOccurrence,我们需要执行重量级计算。如果我们使用复制构造函数WordOccurrences(wordOccurences),我们可以立即得到该计算的结果(在维基百科中,使用克隆方法,但原理是相同的。)