像字段添加或删除这样的类更改是否保持Serializable的向后兼容性?
我有一个关于Java序列化的问题,您可能需要修改可序列化类并保持向后兼容性。
我来自深度C#经验,所以请允许我将Java与.NET进行比较。
在我的Java场景中,我需要使用Java的运行时序列化机制序列化对象,并将二进制数据存储在永久存储中以便将来重用这些对象。 问题是 ,将来,课程可能会发生变化。 可以添加或删除字段。
我不太了解Java序列化,除了这篇关于如何在处理序列化时不用Java编程的精彩文章。 正如我想象的那样(d),serialVersionUID在Java序列化中起着关键作用,这是我需要你帮助的地方。
除了文章的例子(我知道它编码不好),当我在修改类后要求更新它时,该字段是否应该被修改?
我记得在.NET世界中,当我添加新字段时,我必须将[OptionalField]
属性添加到字段以获得向后兼容性,因此CLR不会在旧的序列化数据中要求它。 此外,当我需要弃用字段时,我必须只删除公共方法而不是私有字段。
最佳序列化的准则是什么?
谢谢。
[添加]这是一个例子。 假设我有Foo课
public class Foo { private String bar; }
然后我改为:
public class Foo { private String bar; private Integer eggs; }
这两个版本之间是否兼容? 如果我在编译“newFoo”时反序列化“oldFoo”,那么egg会等于null还是抛出exception? 我更喜欢第一个,显然!!
假设您有一个MyClass
类,并且您希望确保序列化兼容性向前发展,或者至少确保您不会无意中更改其序列化forms。 在大多数情况下,您可以使用GS Collections测试实用程序中的Verify.assertSerializedForm()
。
首先编写一个测试,断言你的类的serialVersionUID
为0L
,并且串行forms为空字符串。
@Test public void serialized_form() { Verify.assertSerializedForm( 0L, "", new MyClass()); }
运行测试。 它将失败,因为String表示Base64编码并且永远不会为空。
org.junit.ComparisonFailure: Serialization was broken.
当您单击以查看差异时,您将看到实际的Base64编码。 将其粘贴到空字符串中。
@Test public void serialized_form() { Verify.assertSerializedForm( 0L, "rO0ABXNyAC9jYXJhbWVsa2F0YS5zaHVrbmlfZ29lbHZhLkV4ZXJjaXNlOVRlc3QkTXlDbGFzc56U\n" + "hVp0q+1aAgAAeHA=", new MyClass()); }
重新运行测试。 它可能会再次失败并显示如下错误消息。
java.lang.AssertionError: serialVersionUID's differ expected:<0> but was:<-7019839295612785318>
将新的serialVersionUID粘贴到测试中代替0L。
@Test public void serialized_form() { Verify.assertSerializedForm( -7019839295612785318L, "rO0ABXNyAC9jYXJhbWVsa2F0YS5zaHVrbmlfZ29lbHZhLkV4ZXJjaXNlOVRlc3QkTXlDbGFzc56U\n" + "hVp0q+1aAgAAeHA=", new MyClass()); }
现在测试将通过,直到您更改序列化表单。 如果您意外中断测试(更改序列化表单),首先要做的是检查您是否在Serializable类中指定了serialVerionUID
。 如果你把它留下来,JVM会为你生成它并且非常脆弱。
public class MyClass implements Serializable { private static final long serialVersionUID = -7019839295612785318L; }
如果测试仍然中断,您可以尝试通过将新字段标记为瞬态来恢复序列化表单,使用writeObject()等完全控制序列化表单。
如果测试仍然中断,您必须决定是否查找并还原您的更改,这些更改会导致序列化或将更改视为对序列化表单的有意更改。
当您有意更改序列化表单时,您需要更新Base64字符串以使测试通过。 当你这样做时,同时更改serialVersionUID
至关重要 。 你选择的数字并不重要,只要它是你以前从未用过的数字。 惯例是将其更改为2L
,然后是3L
等。如果您从随机生成的serialVersionUID
(例如-7019839295612785318L
中的-7019839295612785318L
)开始,您仍然应该将数字提升为2L
因为它仍然是序列化的第二个版本形成。
注意:我是GS系列的开发人员。
Java的本机序列化支持主要用于短期存储或通过网络传输,因此应用程序的实例可以轻松通信。 如果你是在长期存储之后,我建议你看一下像JAXB这样的XML序列化技术。
当您需要长时间保存数据时,最好不要使用序列化。尝试使用数据库或协议缓冲区 (协议缓冲区是一种以高效且可扩展的格式编码结构化数据的方法)。
如果要管理类的序列化版本,则应实现Externalizable接口,并指定如何序列化和反序列化类的状态。 这样,序列化状态可以比“真实”状态更简单。 例如,TreeMap对象的状态为红黑树,而序列化版本只是键值列表(并且在反序列化对象时重新创建树)。
但是,如果您的类很简单且只有一些可选字段,则可以使用关键字“transient”并使默认序列化忽略它。 例如:
public class Foo { private String bar; private transient Integer eggs; }
不幸的是,我对C#没有深入的了解,但根据你的话,我可以得出结论,Java序列化较弱。 字段serialVersionUID是可选的,只有在您更改了类二进制签名但未更改可序列化字段时才有帮助。 如果您更改了字段,则无法读取先前序列化的对象。
唯一的解决方法是实施自己的searilzation机制。 Java允许这样做。 您必须实现自己的readObject()
和writeObject()
方法。 这些方法应足够智能,以支持向后兼容性。
有关更多详细信息,请参阅java.io.Serializable
javadoc。
如果将serialVersionUID设置为常量(假设为1),则可以自由添加新字段而不会破坏任何内容。 通过在版本之间保持serialVersionUID相同,您告诉序列化算法您知道类是兼容的。