像字段添加或删除这样的类更改是否保持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()

首先编写一个测试,断言你的类的serialVersionUID0L ,并且串行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相同,您告诉序列化算法知道类是兼容的。