Java 序列化和反序列化笔记
Java 序列化和反序列化笔记
一、先理解对象、内存和文件的区别
Java 程序运行时,对象一般存在 JVM 内存里。例如:
User user = new User("Tom", 18); |
这个 user 对象本质上是运行时内存中的一组数据。程序结束后,内存会被释放,对象也就不存在了。
但实际开发中,经常需要把对象保存下来,或者通过网络传给其他系统,例如:
- 把用户信息保存到文件;
- 把对象写入缓存;
- RPC 调用时传输参数;
- 消息队列中传递业务数据;
- 分布式系统之间传输对象状态。
这时就需要把“内存中的对象”转换成“可存储、可传输的数据格式”。
二、什么是序列化和反序列化
序列化:把 Java 对象转换成字节序列的过程。
反序列化:把字节序列重新恢复成 Java 对象的过程。
可以简单理解为:
Java 对象 --序列化--> 字节数据 |
对象在内存中不能直接跨进程、跨机器传输,必须先变成一种稳定的数据表示形式。Java 原生序列化使用的是字节流,JSON、Protocol Buffers、Hessian、Kryo 等也都属于广义上的序列化方案。
三、为什么对象不能直接传输
对象不是一段单纯的文本,它在 JVM 内存中包含对象头、字段数据、引用关系等信息。
比如一个对象里引用了另一个对象:
class Order { |
如果要传输 Order,就不能只传输 Order 自己的字段,还要考虑它引用的 User、OrderItem 等对象。序列化机制需要处理对象图,把相关对象一起转换成可保存或可传输的数据。
这也是为什么 Java 序列化要求对象及其内部引用对象都满足可序列化条件。
四、Serializable 接口的作用
Java 原生序列化中,对象需要实现 Serializable 接口:
import java.io.Serializable; |
Serializable 是一个标记接口,接口里没有方法。它的作用是告诉 JVM:这个类的对象允许被序列化。
如果一个对象没有实现 Serializable,但被 ObjectOutputStream 序列化,就会抛出:
java.io.NotSerializableException |
五、序列化的基本使用
1. 写出对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.dat")); |
2. 读取对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.dat")); |
核心流程就是:
ObjectOutputStream负责把对象写成字节流;ObjectInputStream负责把字节流还原成对象;- 类必须实现
Serializable; - 字段里引用的对象通常也要可序列化。
六、serialVersionUID 的作用
serialVersionUID 是序列化版本号,用来判断“字节流中的类版本”和“当前 JVM 中的类版本”是否兼容。
private static final long serialVersionUID = 1L; |
反序列化时,JVM 会比较两个版本号:
- 如果一致,认为类结构兼容,可以尝试反序列化;
- 如果不一致,会抛出
InvalidClassException。
如果不手动声明 serialVersionUID,JVM 会根据类名、字段、方法等信息自动生成一个值。问题是:只要类结构稍微变化,自动生成的值就可能变化,导致历史数据无法反序列化。
因此,实际开发中建议显式声明:
private static final long serialVersionUID = 1L; |
七、类字段变更对反序列化的影响
假设旧版本类是:
public class User implements Serializable { |
后来新增字段:
public class User implements Serializable { |
只要 serialVersionUID 不变,旧数据一般仍然可以反序列化。新增字段没有历史值,会使用默认值,例如对象类型为 null,基本类型为 0 或 false。
但如果修改了字段类型、删除关键字段,或者改变类继承结构,就可能出现兼容性问题。
八、transient 关键字
被 transient 修饰的字段不会参与序列化。
public class User implements Serializable { |
序列化 User 时,password 不会被写入字节流。反序列化后,password 会是默认值 null。
适合使用 transient 的字段:
- 密码、密钥等敏感信息;
- 临时计算结果;
- 不需要持久化的缓存字段;
- 无法或不应该被序列化的资源对象。
九、static 字段不会被序列化
static 字段属于类,不属于某个对象实例,所以不会作为对象状态被序列化。
public class User implements Serializable { |
序列化保存的是对象实例的状态,而不是类本身的状态。反序列化后访问 static 字段,拿到的是当前 JVM 中类变量的值,不是当初写入对象时的值。
十、Externalizable 和 Serializable 的区别
Serializable 默认由 Java 序列化机制自动处理字段。
Externalizable 则要求开发者自己控制写入和读取逻辑:
public class User implements Externalizable { |
区别可以简单记:
| 对比项 | Serializable | Externalizable |
|---|---|---|
| 控制方式 | JVM 默认处理 | 开发者手动控制 |
| 实现成本 | 低 | 高 |
| 灵活性 | 一般 | 更强 |
| 是否需要无参构造 | 不强制要求 | 通常需要 public 无参构造 |
日常开发中更常见的是 Serializable。
十一、JDK 原生序列化的问题
Java 原生序列化虽然使用简单,但在生产环境里并不是最推荐的通用方案。
主要问题:
- 性能一般:序列化和反序列化开销较大;
- 结果体积偏大:生成的字节流往往不够紧凑;
- 跨语言能力差:主要服务于 Java 生态;
- 安全风险高:反序列化不可信数据可能带来安全问题;
- 版本兼容麻烦:类结构变化容易影响历史数据读取。
所以在实际项目中,如果是接口通信或服务间调用,通常会优先考虑 JSON、Protocol Buffers、Hessian、Kryo 等方案。
十二、常见序列化方案对比
| 方案 | 可读性 | 性能 | 体积 | 常见场景 |
|---|---|---|---|---|
| JDK 原生序列化 | 差 | 一般 | 较大 | Java 内部简单持久化 |
| JSON | 好 | 一般 | 中等 | HTTP API、配置文件、日志 |
| Protocol Buffers | 差 | 好 | 小 | RPC、跨语言通信 |
| Hessian | 差 | 较好 | 较小 | Java 服务间调用 |
| Kryo | 差 | 好 | 小 | 高性能 Java 内部序列化 |
选择时可以按这个思路:
- 要可读性:优先 JSON;
- 要跨语言和性能:考虑 Protocol Buffers;
- Java 内部高性能传输:考虑 Kryo、Hessian;
- 简单学习或临时保存对象:可以了解 JDK 原生序列化。
十三、面试常问点
1. Serializable 为什么没有方法?
因为它是标记接口,只用于标记这个类允许被序列化。真正的序列化逻辑由 ObjectOutputStream 和 JVM 底层机制完成。
2. transient 和 static 为什么不会被序列化?
transient 是主动声明字段不参与序列化。
static 属于类变量,不属于对象实例。序列化保存的是对象状态,所以不会保存 static 字段。
3. serialVersionUID 不写可以吗?
可以,但不推荐。不写时 JVM 会自动生成,类结构变化后可能导致版本号变化,从而让旧数据无法反序列化。实际开发中建议显式声明。
4. 序列化对象里有另一个对象怎么办?
如果字段引用了其他对象,这些对象通常也要实现 Serializable。否则序列化时可能抛出 NotSerializableException。
5. 为什么生产中很少直接用 JDK 原生序列化?
因为它性能一般、字节流较大、跨语言能力弱,而且反序列化不可信数据存在安全风险。实际项目中更常用 JSON、Protocol Buffers、Hessian、Kryo 等方案。
十四、小结
Java 序列化解决的是“对象如何离开 JVM 内存继续保存或传输”的问题。
学习时重点记住:
- 序列化是对象到字节流;
- 反序列化是字节流到对象;
Serializable是标记接口;serialVersionUID用于版本兼容;transient字段不参与序列化;static字段不属于对象状态;- 生产中要谨慎使用 JDK 原生序列化。

