中 进阶
序列化与反序列化#
一句话答案#
序列化将对象转为字节流用于传输/存储,Java 用 Serializable 接口,生产推荐 JSON 或 Protobuf(跨语言、更高效)。
核心要点
Java序列化: 实现Serializable + serialVersionUID
方案对比:
| 方案 | 速度 | 体积 | 跨语言 |
|---|---|---|---|
| Java原生 | 慢 | 大 | 否 |
| JSON | 中 | 中 | 是 |
| Protobuf | 快 | 小 | 是(推荐) |
原生序列化机制(深挖点):
- 钩子方法
writeObject/readObject:类可定义private void writeObject(ObjectOutputStream)和readObject(ObjectInputStream),ObjectOutputStream通过反射调用它们,从而自定义序列化逻辑(如先defaultWriteObject()写默认字段,再补写 transient 字段——HashMap/ArrayList就这么只序列化有效元素而非整个底层数组)。 - Serializable vs Externalizable:
Serializable全自动,JVM 反射遍历字段,无需写代码,但慢、控制力弱。Externalizable强制实现writeExternal/readExternal,完全手动控制读写,更快更紧凑;但反序列化时会先用 public 无参构造方法 new 出实例再填字段(与 Serializable 不同)。
- 对象图与 handle 表:序列化是递归遍历整个对象引用图。为处理循环引用 / 共享引用,流内部维护一张 handle 表:每个对象第一次写入分配一个 handle,再次遇到同一对象只写 handle 引用,不重复写——既防无限递归,又保证反序列化后共享关系不变。
- 反序列化为何是攻击面(gadget chain 原理):
readObject不走目标类的构造方法,JVM 直接按字节流分配对象并填字段,绕过了构造器里的校验。攻击者构造恶意字节流,让反序列化过程中被自动调用的readObject/readResolve/finalize等方法,串起一连串”看似无害”的已有类调用(gadget chain),最终拼出Runtime.exec()之类的危险调用实现 RCE(如 Apache Commons Collections 的InvokerTransformer经典链)。防御:不反序列化不可信数据、用ObjectInputFilter白名单校验类、改用 JSON/Protobuf 等不携带类型与方法语义的格式。
面试回答(2分钟版)
序列化是将Java对象转换为字节流以便网络传输或持久化存储,反序列化是将字节流还原为对象。Java原生序列化需要实现Serializable接口,这是一个标记接口没有任何方法,JVM通过它判断对象是否允许被序列化。使用时必须显式声明serialVersionUID,它是版本兼容性校验的依据——如果不声明,JVM会根据类结构自动计算,类有任何改动都会导致反序列化失败。transient关键字可以标记字段不参与序列化,比如密码等敏感信息。但在生产环境中不推荐使用Java原生序列化:第一性能差,序列化后体积大速度慢;第二不支持跨语言;第三存在反序列化安全漏洞,攻击者可以构造恶意字节流执行任意代码。推荐的替代方案是Protobuf(序列化体积最小、速度最快、支持跨语言,适合RPC场景)或JSON(可读性好、跨语言、生态成熟,适合REST API)。在微服务间通信中Dubbo默认用Hessian2,gRPC用Protobuf。
追问与易错
追问方向:
- “serialVersionUID 有什么作用?”→ 序列化版本兼容性校验标识,反序列化时比对 UID 不一致则抛 InvalidClassException;不显式声明时 JVM 根据类结构自动计算,任何改动都会变化
- “transient 关键字的作用?”→ 标记字段不参与序列化,反序列化后该字段为默认值(引用类型 null、基本类型 0/false),常用于密码等敏感信息
- “为什么推荐 Protobuf/JSON 而非 Java 原生序列化?”→ Java 原生序列化不支持跨语言、体积大速度慢、且存在反序列化安全漏洞(可构造恶意字节流执行任意代码)
易错点:
- ❌ 不声明 serialVersionUID——JVM 自动计算,类修改后反序列化失败
- ❌ Java 原生序列化存在安全漏洞(反序列化攻击)——生产环境应避免