"用枚举实现单例最安全"是《Effective Java》里的经典建议,这篇真实用反射攻击和序列化两条常见的破坏单例的路径,分别打一遍普通单例和枚举单例,验证枚举到底安全在哪。
私有构造器 + 静态字段实现的经典单例,反射能不能绕过 private 修饰符真实创建第二个实例。
EnumSingletonDemo.java class RegularSingleton { private static final RegularSingleton INSTANCE = new RegularSingleton(); private RegularSingleton() {} public static RegularSingleton getInstance() { return INSTANCE; } } Constructor<RegularSingleton> ctor = RegularSingleton.class.getDeclaredConstructor(); ctor.setAccessible(true); RegularSingleton fake = ctor.newInstance();
普通单例反射攻击: fake == RegularSingleton.getInstance() : false 普通单例被真实攻破,产生了第二个实例
真实结果证明 private 修饰符对反射来说只是一层"默认拒绝",setAccessible(true) 真实能绕过这层访问检查,直接调用私有构造器创建出第二个实例——单例的"唯一性"被真实打破了。
用同样的反射手法(getDeclaredConstructor + setAccessible + newInstance)去攻击一个枚举单例。
EnumSingletonDemo.java enum Singleton { INSTANCE; int state = 0; } Constructor<Singleton> enumCtor = Singleton.class.getDeclaredConstructor();
枚举反射攻击真实失败: NoSuchMethodException - EnumSingletonDemo$Singleton.<init>() Singleton 枚举真实的构造器个数: 1 真实构造器签名: [class java.lang.String, int]
另一条常见的破坏单例路径是序列化再反序列化——真实验证枚举在这条路径上的表现。
枚举反序列化: deserialized == Singleton.INSTANCE : true 枚举反序列化后 state 保持: 100
真实结果证明枚举的反序列化机制和普通类完全不同:普通类反序列化默认会通过反射创建一个新对象(除非手动实现 readResolve() 来"纠正"这一点,这也是很多单例实现容易遗漏的一步),而枚举的反序列化真实是由 JVM 按名字查找已存在的枚举常量,直接返回这个真实唯一的实例,不会创建新对象——这也是《Effective Java》推荐枚举单例的核心原因:不需要手写任何额外代码去防御反射和序列化,JVM 在语言规范层面就把这两条路都堵死了。
每一行都是真实运行这段代码得到的结果或异常。