【问题标题】:Java generics change on class field and serializationJava 泛型在类字段和序列化上的变化
【发布时间】:2019-06-14 14:46:44
【问题描述】:

我有一个 Java 序列化问题,它在 Java Docs 中既没有报告也没有否定为有问题(针对JDK8)。

所以,假设我有一个实现Serializable的类

private static final long serialVersionUID = 1L;

private List<CharSequence> values;

如果我将values 更改为

private List<String> values;

我应该更改serialVersionUID吗?

【问题讨论】:

  • 可能有人序列化了列表中包含非字符串实例的实例。所以,是的。为什么需要在文档中明确提及?
  • @QingfeiYuan 我不同意。答案指向我提到的来自 Oracle 的相同文档。对泛型的关注点变化什么也没说。
  • @AndyTurner 是的,我明白了。因此,在运行时反序列化将惨遭失败。相反的呢?那么,如果我从 List&lt;String&gt; 更改为 List&lt;CharSequence&gt; 那是一个超类型?它应该可以正常工作吗?

标签: java serialization


【解决方案1】:

另一种看待这种序列化/反序列化操作的方式是,它有点像强制转换。

您可以将List&lt;CharSequence&gt; 转换为List&lt;String&gt; 吗?不是直接的:编译器会阻止你,因为泛型是不变的。但是,您可以强制它:

List<String> list = (List<String>) (List<?>) listOfCharSequences;
String entry = list.get(0);

而且这个可能在很多时候都有效,因为String 是最常见的CharSequence。

但其中可能还有其他内容,即CharSequence,但不是String,例如StringBuilder。上面的代码会因ClassCastException 而崩溃。

因此,如果您可以确定您的所有CharSequences 都是真正的Strings,那么进行此更改是安全的。否则,您应该通过更改序列版本 ID 来防范它;或者,对于String,您可以简单地在所有元素上调用toString():

List<String> listOfStrings = listOfCharSequences.stream()
    .map(cs -> Objects.toString(cs, null))
    .collect(toList())

(String.toString() 返回自身,因此对于已经是 Strings 的元素,这将不会返回新实例)

【讨论】:

  • 我同意,感谢您的澄清。你认为相反的情况会起作用吗?那么,将字段从类型更改为其超类型?
  • @JeanValjean 这取决于运行旧版本代码的东西是否会读取由新版本代码编写的实例。这与原始问题的情况基本相同:如果任何期望 String 的内容读取其他内容,它将中断。
【解决方案2】:

如果您想进行不兼容的更改,您应该更改serialVersionUID,这样您会得到一个适当的(如果不是很好)错误,而不是在某个时候出现ClassCastException。

如果要保持兼容性,请保留serialVersionUID 并添加自定义readObject 方法将List&lt;CharSequence&gt; 转换为List&lt;String&gt;。

编辑:进一步扩展,如果你想要兼容性,你的代码应该是这样的:

// final - for one thing I don't want to implement readObjectNoData
public final class JeanObject {

如果这个类作为子类添加到另一个类中,那么遗留的序列化流中将“没有数据”。

    private static final long serialVersionUID = 1L;

    // Mutable field for Java Serialization, but treat as final.
    private List<String> values = new ArrayList<>();

如果您希望values 成为final,您将需要一个退化的序列化代理 hack,其中代理对象属于同一类型。

我假设您希望集合对象是可变的。您可能不应该序列化可变对象,但这就是它通常的使用方式以及 OP 示例的感​​觉。

    private void readObject(
        ObjectInputStream in
    ) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField fields = in.readFields();
        @SuppressWarnings("unchecked")
        var oldValues = (List<CharSequence>)fields.get("values", null);
        // Don't bother with nice error message.
        //   NPE if missing or null elements.
        this.values = oldValues.stream().collect(
            ArrayList::new,
            (values, value) -> values.add(value.toString()),
            List::addAll
        );
    }
    // ...

(非常功能性)3-arg Stream.collect 的替代方法是使用Collectors.mapping。老式的豪华 for 循环会全面减少混乱,但这并没有在互联网上炫耀 - 你看起来像一个只是完成工作的人。

Collectors.toList 可能不适合,例如,将元素添加到列表中可能会导致错误。也许不是今天。也许不是明天。但是当你的继任者自动更新实现时。他们会恨你一辈子。以下是 API 文档:

不保证类型、可变性、可序列化或 List 的线程安全返回

谁想要那个?

编辑:再次阅读该引用,如果您使用 toList...,您可能无法序列化您的反序列化对象...

(代码可以编译,但与以往一样,未经测试。)

【讨论】:

    【解决方案3】:

    这完全取决于您。当您指定一个 serialVersionUID 时,您将负责在需要时更改它。 如果您的系统在需要更改此对象时有可能遇到该对象的各种版本。

    【讨论】:

      猜你喜欢
      • 2015-05-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-29
      • 2014-12-20
      • 1970-01-01
      相关资源
      最近更新 更多