像字段添加或删除这样的类更改是否保持Serializable的向后兼容性?

时间:2011-06-16 15:43:20

标签: java serialization

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

我来自深度C#经验,所以请允许我将Java与.NET进行比较。

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

我不了解Java序列化,除了这篇关于not to program in 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还是抛出异常?我更喜欢第一个,显然是!!

6 个答案:

答案 0 :(得分:11)

假设您有一个类MyClass,并且您希望确保序列化兼容性向前发展,或者至少确保您不会无意中更改其序列化表单。在大多数情况下,您可以使用GS Collections test utilities中的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,
    "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)开始的,那么您应该仍然将数字提升到2L,因为它仍然是序列化表单的第二个版本。

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

答案 1 :(得分:2)

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

答案 2 :(得分:2)

Java的本机序列化支持主要用于短期存储或通过网络传输,因此应用程序的实例可以轻松通信。如果您需要长期存储,我建议您查看一些XML序列化技术,如JAXB。

答案 3 :(得分:2)

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

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

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

答案 4 :(得分:1)

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

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

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

答案 5 :(得分:0)

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