类更改(如字段添加或删除)是否保持可序列化的向后兼容性?



在您可能需要修改可序列化类并保持向后兼容性的情况下,我有一个关于Java序列化的问题。

我有丰富的C#经验,所以请允许我将Java与.NET.进行比较

在我的Java场景中,我需要使用Java的运行时序列化机制来序列化对象,并将二进制数据存储在永久存储中,以便将来重用这些对象问题是将来类可能会发生更改。可以添加或删除字段。

我对Java序列化一无所知,除了这篇关于如何在处理序列化时不使用Java编程的精彩文章。正如我想象的(d),serialVersionUID在Java序列化中起着关键作用,这就是我需要您帮助的地方。

除了本文的例子(我知道这是糟糕的编码),当Eclipse在我修改类后要求更新该字段时,是否应该不修改该字段?

我记得在.NET世界中,当我添加新字段时,我必须将[OptionalField]属性添加到字段中以获得向后兼容性,因此CLR在旧的序列化数据中不需要它。此外,当我需要弃用一个字段时,我只能删除公共方法,而不能删除私有字段。

最佳序列化的指导原则是什么?

谢谢。

[Add]下面是一个例子。假设我有Foo 类

public class Foo {
    private String bar;
}

然后我改为:

public class Foo {
    private String bar;
    private Integer eggs;
}

这两个版本之间的兼容性坏了吗?如果在编译"newFoo"时反序列化"oldFoo",eggs是等于null还是引发异常?很明显,我更喜欢第一个!!

假设您有一个类MyClass,并且您希望确保串行化兼容性,或者至少确保您不会无意中更改其串行化形式。在大多数情况下,可以使用GS Collections测试实用程序中的Verify.assertSerializedForm()

首先编写一个测试,断言类的serialVersionUID0L,并且具有空字符串的串行形式。

@Test
public void serialized_form()
{
  Verify.assertSerializedForm(
    0L,
    "",
    new MyClass());
}

运行测试。它将失败,因为String表示Base64编码并且从不为空。

org.junit.ComparisonFailure: Serialization was broken. <Click to see difference>

当您单击以查看差异时,您将看到实际的Base64编码。将其粘贴到空字符串中。

@Test
public void serialized_form()
{
  Verify.assertSerializedForm(
    0L,
    "rO0ABXNyAC9jYXJhbWVsa2F0YS5zaHVrbmlfZ29lbHZhLkV4ZXJjaXNlOVRlc3QkTXlDbGFzc56Un"
      + "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,
    "rO0ABXNyAC9jYXJhbWVsa2F0YS5zaHVrbmlfZ29lbHZhLkV4ZXJjaXNlOVRlc3QkTXlDbGFzc56Un"
      + "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)开始,则仍然应该将数字提升为2L,因为它仍然是序列化形式的第二个版本。

注:我是GS系列的开发人员。

Java的本机序列化支持主要适用于通过网络进行短期存储或传输,因此应用程序实例可以毫不费力地进行通信。如果您想要长期存储,我建议您了解一些XML序列化技术,如JAXB。

当您需要长时间保存数据时,最好不要使用序列化。尝试使用数据库或协议缓冲区(协议缓冲区是一种以高效但可扩展的格式对结构化数据进行编码的方式)。

如果您想管理类的序列化版本,您应该实现接口Externalizable,并指定如何序列化和反序列化类的状态。这样,序列化状态可以比"真实"状态更简单。例如,TreeMap对象的状态为红黑树,而序列化版本只是键值列表(当对象被反序列化时,树会被重新创建)。

但是,如果您的类很简单,并且只有一些可选字段,则可以使用关键字"transient"并使默认序列化忽略它。例如:

public class Foo {
    private String bar;
    private transient Integer eggs;
}

不幸的是,我对C#没有深入的了解,但根据您的话,我可以得出Java序列化较弱的结论。字段serialVersionUID是可选的,只有在更改了类二进制签名但未更改可序列化字段时才有帮助。如果更改了字段,则无法读取以前序列化的对象。

唯一的解决方法是实现您自己的烧录机制。Java允许这样做。您必须实现自己的readObject()writeObject()方法。这些方法应该足够智能,以支持向后兼容性。

有关更多详细信息,请参阅java.io.Serializable的javadoc。

如果将serialVersionUID设置为常量(假设为1),则可以自由添加新字段,而不会破坏任何内容。通过在不同版本之间保持serialVersionUID不变,您就告诉序列化算法知道这些类是兼容的。

最新更新