【问题标题】:Is Object deserialization a proper way to implement Prototype pattern in Java?对象反序列化是在 Java 中实现原型模式的正确方法吗?
【发布时间】:2014-07-16 13:48:34
【问题描述】:

TL;DR

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

注意

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


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

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

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

  • 避免以标准方式(例如,使用“new”关键字)创建新对象的内在成本,因为它对于给定的应用程序来说过于昂贵。

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

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

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

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

所以,我只是问是否通过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 中复制对象的安全、常规方式。
  • @HotLicks 问题已更新。
  • @Chronio 我的问题根本不是关于复制构造函数的。我想知道对象反序列化是否是实现原型模式的另一种选择。而且你没有在你的 cmets 中解决这个问题。是的,我有大对象要复制,由于系统维护,哪些字段可能会因周期而异。在这种情况下,更新大量可以加速应用程序以在 0.582 毫秒内(例如)获得深拷贝的类并不比使用对象序列化/反序列化更好谁向我展示了几乎那么快,就像 JIT 没有启动时的 1.042 毫秒。
  • @HotLicks 我没有为任何事情辩解。我只是想知道这是否是原型模式的有效实现。如果您非常确定自己的答案,请发布。

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


【解决方案1】:

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

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

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

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

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

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

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

因此,除非您正在使用隐藏其合理构造函数的第三方库,否则请忘记那个性能指标。 Prototype 的重点在于

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

【讨论】:

  • 在我的例子中,所有这些第三方库类都实现了Serializable 接口并没有很好地指定,所以我可以序列化它们。
  • 我修改了,但留在了你不拥有代码的场景中,所以答案更普遍有用。
  • Prototype 模式将复制的责任放在要复制的对象上,如果客户端需要在 Java 中提供反序列化和序列化代码,则不是这种情况,您可以在类中添加writeObjectreadObject 方法来定义被序列化/反序列化时的行为,因此,根据您的解释,这使得序列化/反序列化过程更接近原型实现。
  • 是的,关闭。每个客户端都需要提供序列化/反序列化代码,这使得普通的序列化/反序列化不是 Prototype 的干净实现。
  • 当您序列化/反序列化对象时,是定义行为的要序列化/反序列化的对象,而不是客户端,它基于您的第一段使序列化/反序列化过程成为原型的良好实现,但你的结论说不是。
【解决方案2】:

你的问题真的很有趣 Luiggi(我投了票是因为这个想法很棒),可惜你没有说出你真正关心的问题。所以我会尽量回答我所知道的,让你选择你认为有争议的:

  • 优点:

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

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

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

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

但问题值得思考。

【讨论】:

  • 我要补充一点,“原型设计”这个词不够严格,所以我们可以说是一种新方法,不管它是否是原型设计。我们甚至不能说原型模式的完成率有多少。最后,尝试遵守某处定义的“原型设计”是没有用的,因为它只是表达使用模式的一个词(准备使用的预定义/预填充/配置对象树)。目标是目标,而不是实施细节。
  • 是的,但我的问题是关于实施细节。正如我在问题中所发布的那样,使用序列化/反序列化假设您知道自己在做什么,并且冒着使用它的风险。
  • 好吧,所以我误解了这个问题:“对象反序列化是在 Java 中实现原型模式的正确方法吗?” :)
  • 我主要关心的是我是否可以通过使用对象序列化/反序列化来实现这种模式,以及实现细节是否重要。当前接受的答案以是的形式涵盖了这一点,但如果您直接使用它,则不会。缺点是您在使用序列化方法时所承担的部分风险。
【解决方案3】:

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

注意:我不认为解组 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 对,无参数构造函数)。对于打破这种模式的类,它需要额外的配置。这种配置的一个好处是它不需要您编辑现有的类,因此您可以使用 JSON 进行克隆,而无需知道正在使用 JSON 的代码的任何其他部分。

我喜欢这种方法而不是序列化的另一个原因是它更易于人工调试(只需查看字符串即可了解数据是什么)。此外,还有大量用于处理 JSON 的工具:

  1. Online JSON formatter
  2. Veiw JSON as HTML based webpage

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

【讨论】:

  • 好点@LuiggiMendoza。我在回答中添加了摘要。
【解决方案4】:

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

【讨论】:

  • 问题询问对象反序列化是否是原型模式的正确实现。你没有解决这个问题。
  • 当然。我认为您可以使用任何方法,只要它避免了创建新对象的标准方法所产生的昂贵计算。我认为这是原型模式背后的主要思想。
猜你喜欢
  • 1970-01-01
  • 2017-03-15
  • 1970-01-01
  • 2011-08-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-05
相关资源
最近更新 更多