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

Java 序列化中,writeReplace 和 readResolve 是两个关键的“拦截点”,它们不参与字段读写,而是在对象生命周期的关键阶段做替换——前者在序列化前换掉原对象,后者在反序列化后换掉新实例。单例保护的核心,正是靠 readResolve 在最后一刻把反序列化出来的新对象替换成唯一的静态实例。
readResolve 是单例防破防的最后一道闸门
反序列化会绕过构造器、初始化块和所有访问控制,直接基于字节流重建对象。这意味着即使你用了双重检查锁或静态内部类,只要类实现了 Serializable,就可能被反序列化出第二个实例。
readResolve 就是为堵住这个漏洞而设:它在对象字段全部赋值完毕(包括 transient 字段设为默认值)之后、返回给调用方之前被 JVM 自动调用。只要你返回的是那个唯一实例(比如 return INSTANCE;),外部拿到的永远是同一个引用。
- 方法必须是 private Object readResolve() throws ObjectStreamException
- 所在类必须实现
Serializable,否则 JVM 不识别 - 返回
null是合法的,结果就是反序列化得到null,不会抛异常 - 它不阻止字段被读取,只决定“最终交给用户的那个对象是谁”
writeReplace 通常不单独用于单例,但可配合演进策略
对纯单例场景,writeReplace 并非必需——它主要用于序列化前将原对象替换成另一个可序列化对象(比如轻量代理)。但在某些增强型单例设计中,它可以提前简化数据结构。例如,一个带缓存、连接池等运行时状态的单例类,你不想把这些敏感或不可序列化的字段落盘,就可以用 writeReplace 返回一个仅含标识符的代理对象。
- 方法签名固定为 private Object writeReplace() throws ObjectStreamException
- 返回对象必须可序列化;返回
null会触发InvalidObjectException - 一旦定义,原类的
writeObject不再自动调用(除非你在代理类里显式转发) - 和 readResolve 成对使用,才能构成完整的“序列化代理模式”
为什么光靠 readResolve 还不够安全?
readResolve 只防御反序列化路径,无法拦截反射攻击。如果攻击者通过 Constructor.setAccessible(true) 调用私有构造器,JVM 不会触发 readResolve。
- 建议在构造器中加入校验逻辑:
if (INSTANCE != null) throw new RuntimeException("Singleton already initialized"); - 所有非 transient 的引用字段,尽量声明为
transient,避免被反序列化过程意外覆盖或泄露 - 确保
INSTANCE是static final或经线程安全方式初始化,防止 readResolve 返回未初始化的 null
更彻底的方案:直接用枚举实现单例
如果单例不需要继承其他类、也不需实现复杂接口,推荐一步到位使用枚举:
- Java 规范强制保证枚举类型的序列化/反序列化天然单例
- JVM 内部硬编码了 readResolve 行为,无需手动编写
- 同时免疫反射、多类加载器、反序列化等全部常见破坏路径
- 写法极简:
public enum Singleton { INSTANCE; }
验证是否生效很简单:序列化一个实例,再反序列化,用 == 比较两个引用——正确实现下结果必为 true。










