readresolve方法通过反序列化末尾替换新对象来保障单例唯一性;它要求类实现serializable、方法为private object readresolve()、返回已初始化的静态实例,且不防反射攻击。

readResolve 方法通过在反序列化完成后的“最后一刻”替换新创建的对象,来守住单例的唯一性。它不阻止反序列化过程本身,而是在 JVM 已经用反射构造出新实例之后,主动把那个新对象换成早已存在的静态实例——所以 == 比较始终为 true。
readResolve 的触发机制
当类实现 Serializable 接口,并且反序列化流程走到末尾时,JVM 会检查该类是否声明了符合规范的 readResolve 方法。一旦满足全部条件,就调用它,再用其返回值覆盖刚生成的对象。
- 必须是 private 修饰(不能是 public/protected/default)
- 签名严格为 private Object readResolve() throws ObjectStreamException
- 返回值必须是那个已初始化好的单例实例,比如 return INSTANCE; 或 return getInstance();
- 方法体里不能依赖尚未初始化的字段,否则可能引发 NPE 或逻辑错误
为什么能防住反序列化破坏
反序列化底层会绕过构造器,直接通过反射新建对象。readResolve 不跟这个过程对抗,而是“事后接管”:
- ObjectInputStream 已经 new 出一个新实例(内存地址不同)
- 紧接着发现类有 readResolve,就执行它
- 你返回的是原始的 INSTANCE,JVM 把新对象替换成它
- 最终用户拿到的,就是和 getInstance() 返回的同一个对象
常见失效原因和规避方式
看似简单,但几个细节容易导致 readResolve 不生效:
- 类实现了 Externalizable 接口:此时 readResolve 被忽略,必须在 readExternal 中手动返回单例
- 用了非标准序列化库(如 Kryo、Jackson):默认不识别 readResolve,需按文档配置支持或注册反序列化器
- 字段被标记为 transient:这些字段反序列化后为 null,readResolve 无法恢复它们,必要时要配合自定义 readObject
- 构造器未做反射防护:readResolve 对反射攻击无效,应在构造器中加非空校验(如 if (INSTANCE != null) throw ...)
比 readResolve 更稳妥的替代方案
如果项目允许且单例不需继承或延迟初始化,直接用 枚举 是最省心的选择:
- JVM 层面硬编码保护,反序列化永远返回已有常量,无需写任何额外方法
- 天然防止反射、序列化、多线程等所有常见破坏手段
- 缺点是无法 extends 其他类,也不能懒加载(所有枚举实例在类加载时就初始化)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











