writereplace 和 readresolve 是 java 序列化中用于对象替换的核心机制:前者在序列化前将原对象替换为代理对象,后者在反序列化后将重建对象替换为指定实例;二者成对使用构成序列化代理模式,实现安全可控的序列化契约。

writeReplace 是序列化前的“临门一脚”替换
它不是用来修改字段或跳过某些数据,而是让 JVM 在真正开始写入字节流之前,把你要序列化的对象换成另一个对象——这个新对象才是最终被写进 ObjectOutputStream 的那个。
典型使用场景:维护单例、避免敏感字段落盘、简化序列化结构(比如只存 ID,其余字段反序列化时重建)。
-
writeReplace必须是private实例方法,返回类型是Object,抛出ObjectStreamException - 返回的对象必须可序列化;如果返回
null,会抛InvalidObjectException - 它不参与构造逻辑,也不影响原对象生命周期——只是“换人上场”
- 注意:一旦用了
writeReplace,原对象的writeObject不再被调用(除非你在代理类里主动调)
示例:CustUserV3 类里只序列化一个 name 字段,其余从 name 推导,就靠 writeReplace 返回一个轻量级 CustUserV3Proxy:
private Object writeReplace() throws ObjectStreamException {
return new CustUserV3Proxy(this.name);
}
readResolve 是反序列化后的“最终拍板”
它发生在对象从字节流中重建完成之后、返回给调用方之前。JVM 会检查当前类有没有 readResolve 方法,如果有,就把刚构造出来的对象扔掉,换成你方法里返回的那个。
立即学习“Java免费学习笔记(深入)”;
最常见用途就是单例模式保全:防止通过反序列化绕过私有构造器,创建第二个实例。
-
readResolve也必须是private实例方法,返回Object,抛ObjectStreamException - 它在反序列化对象的构造器执行完后才调用,所以对象字段已赋值(包括
transient字段为默认值) - 如果返回
null,反序列化结果就是null,不会报错 - 注意:它不能阻止字段被读取,只是“换掉最终返回的对象”
示例(单例):
private Object readResolve() {
return INSTANCE; // 始终返回唯一的静态实例
}
writeReplace + readResolve 组合才是“序列化代理模式”
单独用其中一个,往往解决不了完整问题;成对使用,才能实现安全、可控、可演进的序列化契约。
比如你想把一个含大量缓存字段的类序列化为精简结构,又希望反序列化回来还是原来的类(不是代理类),就得:writeReplace 换成代理 → 序列化代理 → readResolve 把代理还原成目标类的新实例。
- 代理类本身通常不实现
Serializable,或者只带必要字段,避免循环引用 - 代理类的
readObject/writeObject不会被触发,除非它自己也定义了 - 如果代理类和主类版本不同步,容易出现
ClassNotFoundException—— 这时候readObjectNoData可以兜底,但无法替代readResolve - 别忘了加
serialVersionUID,否则代理类变更时,老数据可能直接反序列化失败
为什么不用 transient 就非得上 writeReplace?
transient 确实能跳过字段,但它太粗暴:字段反序列化后就是 null 或 0,没法动态重建、校验或延迟加载。
而 writeReplace 和 readResolve 提供的是“语义层控制权”——你决定序列化什么、怎么建模、谁来负责恢复逻辑。
-
transient字段在反序列化后永远丢失原始含义;writeReplace可以把它编码进其他字段(比如把address转成geoHash存) - 如果字段依赖外部服务(如数据库连接、线程池),
transient只能置空,readResolve却可以重新注入 - 调试时容易忽略:这两个方法不走常规调用栈,IDE 断点可能不触发,得靠日志或
System.out.println确认是否执行
真正难的不是写这两个方法,而是想清楚:你到底想把什么状态持久化,以及这个状态在将来是否还能被正确解释。这点,任何代理机制都救不了设计模糊的模型。










