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



TL;DR

我是否可以使用 Java 序列化/反序列化Serializable接口、ObjectOutputStreamObjectInputStream 类,并可能在实现Serializable作为原型模式的有效实现的类中添加readObjectwriteObject

注意

这个问题不是讨论使用复制构造函数是否比序列化/反序列化更好。


我知道原型模式的概念(来自维基百科,强调我的(:

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

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

  • 避免以标准方式创建新对象的固有成本(例如,使用"new"关键字(,因为对于给定的应用程序来说,新对象非常昂贵。

从这个问答:Java核心库中的GoF设计模式示例中,BalusC解释说,只有当类实现Cloneable接口(类似于序列化/反序列化对象的标记接口类似于Serializable的标记接口(时,Java中的原型模式才由Object#clone实现。使用此方法的问题在博客文章/相关问答中指出,如下所示:

  • 复制构造函数与克隆
  • Java:推荐的深度克隆/复制实例的解决方案

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

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

AFAIK 在不调用其构造函数的情况下创建对象的唯一方法是反序列化,如此问题的接受答案示例中所述:在序列化和反序列化期间如何调用构造函数?

因此,我只是在问,通过ObjectOutputStream使用对象反序列化(并知道您在做什么,将必要的字段标记为transient并了解此过程的所有含义(或类似的方法是否是原型模式的正确实现。

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


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

//fields is an abbreviation for primitive type and String type fields
//that can vary between 1 and 20 (or more) declared fields in the class
//and all of them will be filled during application execution
class CustomerType {
    //fields...
}
class Customer {
    CustomerType customerType;
    //fields
}
class Product {
    //fields
}
class Order {
    List<Product> productList;
    Customer customer;
    //fields
}
class InvoiceStatus {
    //fields
}
class Invoice {
    List<Order> orderList;
    InvoiceStatus invoiceStatus;
    //fields
}
//class to communicate invoice data for external systems
class InvoiceOutboundMessage {
    List<Invoice> invoice;
    //fields
}

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

直接使用 Java 对象序列化并不完全是原型模式,但序列化可用于实现该模式。

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

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

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

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

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

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

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

  • 它允许客户端在不知道其类的情况下复制对象实例,并且
  • 它无需创建工厂层次结构即可实现该目标,就像使用 AbstractFactory 模式满足相同的目标一样。

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

注意:我不认为解组 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. 在线 JSON 格式化程序
  2. Veiw JSON 作为基于 HTML 的网页

而用于 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 的方法。

你的问题真的很有趣路易吉(我投了赞成票,因为这个想法很棒(,你不说你真正关心的是什么很可怜。所以我会试着回答我所知道的,让你选择你认为有争议的:

  • 优势:

      在内存使用
    • 方面,使用序列化将获得非常好的内存消耗,因为它以二进制格式序列化您的对象(而不是以 json 或更糟的文本形式序列化:xml(。您可能需要选择一种策略,以在需要时将对象"模式"保留在内存中,并将其保留为"较少使用的第一持久化"策略或"先使用先持久化">
    • 编码非常直接。有一些规则需要尊重,但是您没有很多复杂的结构,这仍然是可维护的
    • 不需要外部库,这在具有严格安全/法律规则的机构中是一个相当大的优势(对要在程序中使用的每个库进行验证(
    • 如果您不需要在程序版本/JVM版本之间维护对象。您可以从每个JVM更新中获利,因为速度是Java程序的真正关注点,并且与io操作(JMX,内存读/写,nio等(非常相关。因此,新版本很有可能具有优化的io/内存使用/序列化算法,您会发现您无需更改代码即可更快地编写/阅读。
  • 弊:

    • 如果您更改树中的任何对象,则会丢失所有原型。序列化仅适用于相同的对象定义
    • 你需要反序列化一个对象以查看它里面的内容:而不是原型模式,如果你从Spring/Guice配置文件中获取它,它是"自我记录的"。保存到磁盘的二进制对象非常不透明
    • 如果你打算做一个可重用的库,你就要给你的库用户强加一个非常严格的模式(在每个对象上实现可序列化,或者对不可序列化的 dields 使用瞬态(。此外,编译器无法检查此约束,您必须运行程序以查看是否有问题(如果树中的对象对于测试为 null,则可能不会立即看到(。当然,我将其与其他原型技术进行比较(例如Guice的主要功能是编译时检查,Spring最近也这样做了(

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

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

但这个问题值得思考。

在某些情况下,使用复制构造函数创建新对象与"以标准方式"创建新对象不同。您的问题的维基百科链接中解释了一个例子。在该示例中,要使用构造函数 WordOccurrences(text, word( 创建新的 WordOccurrences,我们需要执行重量级计算。如果我们使用复制构造函数 WordOccurrences(wordOccurrenceences(,我们可以立即获得该计算的结果(在维基百科中,使用 clone 方法,但原理是相同的(。

最新更新