记录序列化

记录是名义元组 - 一个透明的、浅不可变的载体,用于特定有序元素序列。记录类有很多有趣的地方,正如 Brian Goetz 在他的 重点文章 中所述,但在这里我们将重点关注一个鲜为人知的方面,即记录序列化,以及它与(我认为比)普通类的序列化有何不同。

虽然序列化的概念很简单,但考虑到可以应用的各种自定义,它往往会很快变得复杂。对于记录,我们希望保持尽可能简单直观,因此

  1. 记录对象的序列化仅基于其状态组件。

  2. 记录对象的反序列化仅使用规范构造函数。

第一点的一个结果是,无法自定义记录对象的序列化形式 - 序列化形式基于状态组件,并且仅基于状态组件。此限制简化了模型,因为序列化形式易于理解 - 它就是记录的状态组件。

第二点与反序列化过程的机制有关。假设反序列化正在读取普通类(不是记录类)对象的字节。反序列化将通过调用超类的无参数构造函数来创建一个新对象,然后使用反射将从流中反序列化的值设置为对象的字段。这是不安全的,因为普通类没有机会验证来自流的值。结果可能是一个“不可能”的对象,该对象永远无法通过普通的 Java 程序创建。对于记录,反序列化工作方式不同。反序列化通过调用记录类的规范构造函数来创建一个新的记录对象,并将从流中反序列化的值作为参数传递给规范构造函数。这是安全的,因为它意味着记录类可以在将值分配给字段之前验证这些值,就像普通 Java 程序通过new创建记录对象一样。“不可能”的对象是不可能的。这是可实现的,因为记录组件、规范构造函数和序列化形式都是已知且一致的。

可序列化记录利用记录类提供的保证,提供更简单、更安全的序列化模型。

记录的序列化工作原理

我们将基于 Brian 文章中概述的示例,即一个简单的类来模拟整数范围,从低端到高端。但是,为了比较,我们将使用普通类编写 Range 的实现,以及使用记录类的另一个等效实现。

Range 的具体实现使用一个实现了Serializable的普通类,因为我们希望能够序列化它的实例。Range 有一个低端lo和一个高端hi

public class RangeClass implements Serializable {
    private static final long serialVersionUID = -3305276997530613807L;
    private final int lo;
    private final int hi;
    public RangeClass(int lo, int hi) {
        this.lo = lo;
        this.hi = hi;
    }
    public int lo() { return lo; }
    public int hi() { return hi; }
    @Override public boolean equals(Object other) {
        if (other instanceof RangeClass that
                && this.lo == that.lo && this.hi == that.hi) {
            return true;
        }
        return false;
    }
    @Override public int hashCode() {
        return Objects.hash(lo, hi);
    }
    @Override public String toString() {
      return String.format("%s[lo=%d, hi=%d]", getClass().getName(), lo, hi);
    }
}

注意equalshashCodetoString的冗长样板代码!

记录类与普通类一样,通过实现Serializable来实现可序列化。RangeClass的等效记录对应项如下所示

public record RangeRecord (int lo, int hi) implements Serializable { }

请注意,无需向RangeRecord添加任何额外的样板代码以使其可序列化。具体来说,无需添加serialVersionUID字段,因为记录类的serialVersionUID0L(除非显式声明),并且对于记录类,匹配serialVersionUID值的 requirement 被免除。在极少数情况下,为了在普通类和记录类之间实现迁移兼容性,可能会声明serialVersionUID,有关更多详细信息,请参阅 Java 对象序列化规范的第 5.6.2 兼容更改 节。

好的,让我们序列化一个RangeClass对象和一个RangeRecord对象,两者都具有相同的高端和低端值。

import java.io.*;

public class Serialize {
  public static void main(String... args) throws Exception {
    try (var fos = new FileOutputStream("serial.data");
         var oos = new ObjectOutputStream(fos)) {
      oos.writeObject(new RangeClass(100, 1));
      oos.writeObject(new RangeRecord(100, 1));
    }
  }
}
import java.io.*;

public class Deserialize {
  public static void main(String... args) throws Exception {
    try (var fis = new FileInputStream("serial.data");
         var ois = new ObjectInputStream(fis)) {
      System.out.println(ois.readObject());
      System.out.println(ois.readObject());
    }
  }
}

运行序列化和反序列化程序,我们得到

java --enable-preview Serialize
java --enable-preview Deserialize
RangeClass[lo=100, hi=1]
RangeRecord[lo=100, hi=1]

糟糕!你发现错误了吗?低端值实际上高于高端值。这是不允许的。

我们想要的 invariant 是:范围的低端不能高于高端。让我们将该 invariant 编码到具体类的构造函数中。

public class RangeClass implements ... {
    // ...
    public RangeClass(int lo, int hi) {
        if (lo > hi)
            throw new IllegalArgumentException(String.format("%d, %d", lo, hi));
        this.lo = lo;
        this.hi = hi;
    }
    // ..
}

以及记录等效项。

public record RangeRecord (int lo, int hi) implements ... {
    public RangeRecord {
        if (lo > hi)
            throw new IllegalArgumentException(String.format("%d, %d", lo, hi));
    }
}

请注意,上面的代码使用规范构造函数声明的紧凑版本,允许省略样板分配。

现在,让我们再次运行Deserialize程序,因为它将尝试反序列化serial.data文件中的流对象,并且我们知道这些流对象具有 100 的低端值和 1 的高端值。

java --enable-preview Deserialize
RangeClass[lo=100, hi=1]
Exception in thread "main" java.io.InvalidObjectException: 100, 1
	at java.base/java.io.ObjectInputStream.readRecord(ObjectInputStream.java:2296)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2183)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1685)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:499)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:457)
	at Deserialize.main(Deserialize.java:9)
Caused by: java.lang.IllegalArgumentException: 100, 1
	at RangeRecord.<init>(RangeRecord.java:6)
	at java.base/java.io.ObjectInputStream.readRecord(ObjectInputStream.java:2294)
	... 5 more

首先,我们可以看到一个RangeClass对象被反序列化了,即使新创建的对象违反了构造函数 invariant。这乍一看可能很奇怪,但如前所述,对类为普通类(不是记录类)的对象进行反序列化,会通过调用(第一个不可序列化)超类的无参数构造函数来创建对象,在本例中为java.lang.Object。当然,Java 程序Serialize不可能为 RangeClass 对象生成这样的字节流,因为程序必须使用带有 invariant 检查的两个参数构造函数。但是,请记住,反序列化只是对字节流进行操作,这些字节流在某些情况下可能来自几乎任何地方。

其次,RangeRecord流对象未能反序列化,因为其流字段值(低端和高端)违反了构造函数中的 invariant 检查。这很好,实际上是我们想要的 - 反序列化通过规范构造函数进行。

可序列化类可以创建一个新的对象,而无需调用其构造函数之一,这一事实经常被忽视,即使是经验丰富的开发人员也是如此。通过调用遥远的无参数构造函数创建的对象会导致运行时出现意外行为,因为不会执行反序列化类构造函数中的 invariant 检查。但是,无法利用记录对象的反序列化来创建“不可能”的对象。

有关更多详细信息,请参阅 Java 对象序列化规范的预览相关规范更改文档:https://docs.oracle.com/en/java/javase/14/docs/specs/records-serialization.html

如上所述的完整源代码可以在这里找到:这里

~