面试知识库
进阶

序列化与反序列化#

一句话答案#

序列化将对象转为字节流用于传输/存储,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 原生序列化存在安全漏洞(反序列化攻击)——生产环境应避免