序列化与反序列化的单例模式
在上一篇文章中 ,我谈到了一般的序列化。 这是更加集中的内容,并提供了一个细节: 序列化代理模式 。 这是处理序列化中许多问题的一种好方法,通常是最好的方法。 如果开发人员只想了解这一主题,我会告诉他。
总览
这篇文章的重点是在给出两个简短的示例之前,最后介绍模式的详细定义,最后讨论其优缺点。
据我所知,该模式首先在约书亚·布洛赫(Joshua Bloch)的出色著作《 有效的Java》 (第1版:第57条;第2版:第78条 )中定义。 这篇文章主要重申了那里的说法。
本文中使用的代码示例来自我在GitHub上创建的演示项目 。 查看更多详细信息!
序列化代理模式
此模式应用于单个类,并定义其序列化机制。 为了更容易阅读,以下文本将分别将该类或其实例称为原始一个或多个实例。
序列化代理
顾名思义,模式的关键是序列化代理 。 它被写入字节流,而不是原始实例。 反序列化之后,它将创建原始类的实例,该类将在对象图中取代。
目的是设计代理,使其成为原始类的最佳逻辑表示形式 。
实作
SerializationProxy
是原始类的静态嵌套类。 它的所有字段均为final,唯一的构造函数将原始实例作为唯一的参数。 它提取该实例状态的逻辑表示并将其分配给自己的字段。 由于原始实例被认为是“安全的”,因此无需进行一致性检查或防御性复制。
原始类和代理类都实现Serializable。 但是,由于前者实际上从未真正写入流中,因此只有后者需要一个流唯一标识符 (通常称为串行版本UID )。
序列化
当要对原始实例进行序列化时,可以通知序列化系统将代理写入字节流。 为此,原始类必须实现以下方法:
用代理替换原始实例
private Object writeReplace() {return new SerializationProxy(this);
}
反序列化
在反序列化时,必须反转从原始实例到代理实例的转换。 这是通过SerializationProxy
中的以下方法实现的,该方法在成功实例SerializationProxy
代理实例后被调用:
将代理转换回原始实例
private Object readResolve() {// create an instance of the original class// in the state defined by the proxy's fields
}
创建原始类的实例将通过其常规API(例如,构造函数)完成。
人工字节流
由于writeReplace
常规字节流将仅包含代理的编码。 但是对于人工流却并非如此! 它们可以包含原始实例的编码,并且由于反序列化这些序列未包括在模式中,因此它无法为这种情况提供任何保护措施。
实际上,对此类实例进行反序列化实际上是不需要的,必须防止。 这可以通过让原始类中的方法(在这种情况下被调用)抛出异常来完成:
防止直接反序列化原始实例
private void readObject(ObjectInputStream stream) throws InvalidObjectException {throw new InvalidObjectException("Proxy required.");
}
例子
以下示例是完整演示项目的摘录。 它们只显示多汁的部分,而忽略了一些细节(例如writeReplace
和readObject
)。
复数
一种简单的情况是复数的一种不变类型,称为ComplexNumber
(惊奇!)。 出于本示例的考虑,它在其字段中存储了坐标以及极坐标形式(据说是出于性能方面的考虑):
ComplexNumber –字段
private final double real;
private final double imaginary;
private final double magnitude;
private final double angle;
序列化代理看起来像这样:
ComplexNumber.SerializationProxy
private static class SerializationProxy implements Serializable {private final double real;private final double imaginary;public SerializationProxy(ComplexNumber complexNumber) {this.real = complexNumber.real;this.imaginary = complexNumber.imaginary;}/*** After the proxy is deserialized, it invokes a static factory method* to create a 'ComplexNumber' "the regular way".*/private Object readResolve() {return ComplexNumber.fromCoordinates(real, imaginary);}
}
可以看出,代理不存储极坐标形式的值。 原因是它应该捕获最佳的逻辑表示形式。 并且由于只需要一对值(坐标或极坐标形式)即可创建另一个,因此仅一个序列化了。 这样可以防止存储两个对以实现更好的性能的实现细节通过序列化泄漏到公共API中。
请注意,原始类和代理中的所有字段均为最终字段。 还要注意静态工厂方法的调用,从而无需进行任何附加的有效性检查。
实例缓存
InstanceCache
是一个异构类型安全的容器 ,它使用从类到其实例的映射作为后备数据结构:
InstanceCache –字段
private final ConcurrentMap<Class<?>, Object> cacheMap;
由于映射可以包含任意类型,因此并非所有映射都必须可序列化。 该类的合同规定,足以存储可序列化的类。 因此,有必要过滤地图。 代理的优点是它是所有此类代码的单点:
InstanceCache.SerializationProxy
private static class SerializationProxy implements Serializable {// array lists are serializableprivate final ArrayList<Serializable> serializableInstances;public SerializationProxy(InstanceCache cache) {serializableInstances = extractSerializableValues(cache);}private static ArrayList<Serializable> extractSerializableValues(InstanceCache cache) {return cache.cacheMap.values().stream().filter(instance -> instance instanceof Serializable).map(instance -> (Serializable) instance).collect(Collectors.toCollection(ArrayList::new));}/*** After the proxy is deserialized, it invokes a constructor to create* an 'InstanceCache' "the regular way".*/private Object readResolve() {return new InstanceCache(serializableInstances);}}
利弊
序列化代理模式减轻了序列化系统的许多问题。 在大多数情况下,这是实现序列化的最佳选择,并且应该是实现序列化的默认方法。
优点
这些是优点:
减少语言外特征
该模式的主要优点是它减少了序列化的语言外特征 。 这主要是通过使用类的公共API创建实例来实现的(请参见上面的SerializationProxy.readResolve
)。 因此, 每次创建实例都要经过构造函数,并且始终会执行正确初始化实例所需的所有代码。
这也意味着在反序列化期间不必显式调用此类代码,这可以防止其重复。
对最终字段没有限制
由于反序列化实例是在其构造函数中初始化的,因此此方法不限制哪些字段可以是最终字段(通常是使用自定义序列化形式的情况 )。
灵活的实例化
实际上,代理的readResolve
不必返回与序列化类型相同的实例。 它也可以返回任何子类。
Bloch给出以下示例:
考虑
EnumSet
的情况。 此类没有公共构造函数,只有静态工厂。 从客户端的角度来看,它们返回EnumSet
实例,实际上,它们返回两个子类之一,具体取决于基础枚举类型的大小。 如果基础枚举类型具有64个或更少的元素,则静态工厂将返回RegularEnumSet
; 否则,它们返回JumboEnumSet
。现在考虑一下,如果序列化其枚举类型具有60个元素的枚举集,然后向该枚举类型添加另外五个元素,然后反序列化该枚举集,会发生什么情况。 序列化时它是一个
RegularEnumSet
实例,但反序列化后最好是JumboEnumSet
实例。有效的Java,第二版:p。 314
代理模式使这个琐碎的事情变得很简单: readResolve
仅返回匹配类型的实例。 (这仅在类型符合Liskov替换原理的情况下有效 。)
更高的安全性
它还极大地减少了防止用人工字节流进行某些攻击所需的额外思考和工作。 (假设构造函数已正确实现。)
符合单一责任原则
序列化通常不是类的功能要求,但仍会极大地改变其实现方式。 这个问题无法消除,但至少可以通过更好地分工来减轻。 让类做它的工作,然后让代理处理序列化。 这意味着代理包含有关序列化的所有重要代码,但仅包含其他内容。
与SRP一样 ,这大大提高了可读性。 关于序列化的所有行为都可以在一个地方找到。 而且序列化的表单也更容易发现,因为在大多数情况下,只需查看代理的字段即可。
缺点
Joshua Bloch描述了该模式的一些局限性。
不适合继承
它与客户端可扩展的类不兼容。
有效的Java,第二版:p。 315
是的,就是这样。 没有进一步的评论。 我不太了解这一点,但是我会发现更多…
圆形对象图的可能问题
它与某些对象图包含圆度的类不兼容:如果尝试从对象的序列化代理的
readResolve
方法中调用对象上的方法,则会得到ClassCastException
,因为您还没有对象,只有它序列化代理。有效的Java,第二版:p。 315
性能
代理将构造函数执行添加到序列化和反序列化中。 布洛赫(Bloch)举例说明,这台机器的价格要贵14%。 当然,这不是精确的度量,但是证实了那些构造函数调用不是免费的理论。
反射
我们已经看到了序列化代理模式是如何定义和实现的,以及它的优点和缺点。 应该清楚的是,与默认和自定义序列化相比,它具有一些主要优点,应在适用时使用。
约书亚·布洛赫(Joshua Bloch)的最后一句话:
总之,每当发现自己不得不在其客户端无法扩展的类上编写
readObject
或writeObjet
方法(用于自定义序列化形式)时,请考虑序列化代理模式。 这种模式可能是用非平凡的不变变量稳健地序列化对象的最简单方法。有效的Java,第二版:p。 315
翻译自: https://www.javacodegeeks.com/2015/01/the-serialization-proxy-pattern.html
序列化与反序列化的单例模式