Skip to content

serialVersionUID 深入

你有没有想过这个问题:

serialVersionUID 到底是什么?JVM 怎么知道两个类的 UID 是否「兼容」?

这一节深入探究 UID 的计算规则和版本兼容策略。

UID 是什么

serialVersionUID 是序列化类的版本标识符。它不是一个随机数,而是根据类的「结构」算出来的指纹。

序列化时,这个值写入流;反序列化时,JVM 用流中的 UID 和当前类的 UID 比对——不一致就抛 InvalidClassException

java
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ...
}

显式声明 vs 自动生成

不声明(自动生成)

java
public class User implements Serializable {
    // 没有显式声明 serialVersionUID
    private String name;
}

编译器会根据以下信息自动计算一个 UID:

  • 类名
  • 实现的接口
  • 所有字段(名称、类型、修饰符)
  • 方法签名

这意味着:任何字段或方法的改动,都会导致 UID 变化

java
public class User implements Serializable {
    private String name;
    // 加了一个字段
    private int age; // UID 立刻变了——编译器重新计算
}

这就是为什么很多人发现:明明只加了一个字段,存进去的文件就读不出来了。

显式声明(推荐)

java
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // 字段随便改,UID 不变
}

显式声明后,序列化流中记录的就是这个固定值,字段改动不会影响 UID。

兼容性矩阵

理解了 UID 的计算逻辑,就能明白兼容性规则:

变更类型UID 不变(显式声明)UID 变化(自动生成或手动改)
增加字段✅ 兼容,新字段为默认值❌ 抛 InvalidClassException
删除字段✅ 兼容,旧字段被忽略❌ 抛 InvalidClassException
修改字段类型❌ 不兼容❌ 不兼容
修改字段名❌ 不兼容❌ 不兼容
修改方法✅ 兼容(方法不序列化)✅ 兼容

核心原则:UID 不变 = 结构兼容,可以尝试反序列化;UID 变了 = 结构不兼容,直接拒绝。

典型坑:版本升级

场景一:兼容扩展

java
// V1
public class Message implements Serializable {
    private static final long serialVersionUID = 1L;
    private String content;
}

// V2:新增 timestamp 字段
public class Message implements Serializable {
    private static final long serialVersionUID = 1L; // 保持不变
    private String content;
    private String timestamp; // 新增字段
}

读取 V1 的旧数据:content 有值,timestamp = null(对象字段默认值)。

场景二:破坏性变更

java
public class Message implements Serializable {
    private static final long serialVersionUID = 2L; // ⚠️ 改了 UID
    private String content;
    private int priority; // 从 String 改成了 int
}

读取旧 V1 数据:抛 InvalidClassException。两个 UID 对不上,JVM 直接拒绝。

场景三:字段名变更

java
public class Message implements Serializable {
    private static final long serialVersionUID = 1L;
    private String msg; // 之前叫 content
}

即使 UID 没变,这也是不兼容的——因为 UID 的计算包含字段名。读取旧数据会尝试找 content 字段,找不到就忽略。

UID 冲突问题

如果两个不同类声明了相同的 serialVersionUID

java
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
}

public class Order implements Serializable {
    private static final long serialVersionUID = 1L; // 和 User 一样
}

只要 JVM 能通过类名区分它们(流里记录了类名),就不会有问题。但这不是好实践——每个类应该用不同的 UID。

如何查看自动生成的 UID

serialver 工具(JDK 自带):

bash
serialver User

输出:

User:    private static final long serialVersionUID = -1234567890123456789L;

这个工具计算的就是当前类结构对应的自动生成 UID。可以在发布版本时用它生成初始 UID。

最佳实践

  1. 始终显式声明 serialVersionUID
  2. UID 写在第一行(紧跟 class 声明),方便查找
  3. 不要在兼容变更后修改 UID(如增加字段)
  4. 必须修改 UID 的场景:字段类型变更、字段删除(不兼容)、字段名变更
  5. 不要把 UID 从 1L 开始,建议用 serialver 工具生成实际值

┌─────────────────────────────────────────────────────────────────┐
│  UID 的本质是「类结构的指纹」                                     │
│                                                                 │
│  显式声明 = 固定指纹 = 字段变更不影响 UID = 兼容扩展             │
│                                                                 │
│  口诀:增删字段 UID 不变,类型名变 UID 必须变                     │
└─────────────────────────────────────────────────────────────────┘

基于 VitePress 构建