bigywater.tech
Java · 223

Record 序列化:紧凑数据类的兼容边界

Record 实现 Serializable 之后,反序列化走哪条路径?构造器里的校验会不会生效?数据结构演进时哪些改动兼容?这篇用真实写入、读取的两个进程验证。

0 与 1
反序列化后构造器调用次数:普通类 / record
InvalidObjectException
record 反序列化遇到非法值时的异常
0
record 未声明 serialVersionUID 时的默认值

构造器有没有被调用

实测 · 本机执行输出
  (读取端进程里没有执行过 new P 与 new R)普通 Serializable 类 P:反序列化后 P.calls = 0(构造器没有被调用,字段由序列化机制直接写入);record R:R.calls = 1(canonical 构造器被调用了)
  读到的值:P.x = 1,R.x = 1

校验能不能被绕过

实测 · 本机执行输出
  类 P:构造器有校验,但流里写的是 age=-5 → 读出 age = -5(校验被绕过,对象处于非法状态)
  record R:InvalidObjectException: age < 0;cause = IllegalArgumentException: age < 0

组件演进与 serialVersionUID

实测 · 本机执行输出
  增加组件 c(long) → 读出 R[a=7, b=hello, c=0]
  增加组件 c(String) → 读出 R[a=7, b=hello, c=null]
  删除组件 b → 读出 R[a=7]
  组件 b 改名为 name → 读出 R[a=7, name=null]
  调换组件顺序 → 读出 R[b=hello, a=7]
  组件 a 的类型 int → long → InvalidClassException: R; incompatible types for field a
  组件 a 的类型 int → Integer → InvalidClassException: R; incompatible types for field a
  显式 serialVersionUID 1 → 2(其它不变)→ 读出 R[a=7, b=hello]
  对照:普通类 显式 serialVersionUID 1 → 2 → InvalidClassException: P; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
  record 未声明 serialVersionUID 时,ObjectStreamClass.lookup(R.class).getSerialVersionUID() = 0

自定义序列化方法

实测 · 本机执行输出
  --- 写入端的输出 ---
  [Q.writeReplace 被调用]
  --- 读取端的输出 ---
  R 读出 R[a=1, b=x](上面如果没有 [R.readObject 被调用],说明 record 里的 writeObject/readObject 被忽略)
  [Proxy.readResolve 被调用]
  Q 读出 Q[a=2, b=y]

分步演示

Record 序列化
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;写入与读取分别是两个独立的 JVM 进程,类的两个版本分别编译
    • 没有验证:Externalizable 与 record 的组合、包含数组与集合组件的 record、对象图里的循环引用在 record 中的表现、ObjectInputFilter 与 record 的配合(见前面的过滤器篇目)、JSON 等其它格式的序列化库
    京ICP备2026067502号-1京公网安备11010502063487号