我的直接问题是:既然反射现在受到限制,考虑使用 Enum 进行单例实现是否仍然有意义?
通过单例实现的抛出枚举,我的意思是一些实现,例如:
public enum SingletonEnum {
INSTANCE;
int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
如果我们对比 answer related to scope package access 中提到的模块化的基本思想“...Jigsaw 的可访问性规则现在仅限制对公共(public)元素(类型、方法、字段)的访问”以及枚举修复的反射问题,我们可能想知道为什么仍然将单例编码为枚举。
尽管它很简单,但在序列化枚举时,字段变量不会被序列化。最重要的是,枚举不支持延迟加载。
总而言之,假设我上面没有说任何愚蠢的事情,因为使用枚举作为单例的主要优点是防止反射风险,我会得出这样的结论:将单例编码为枚举并不比围绕静态方法的简单实现如下:
何时需要序列化
public class DemoSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private DemoSingleton() {
// private constructor
}
private static class DemoSingletonHolder {
public static final DemoSingleton INSTANCE = new DemoSingleton();
}
public static DemoSingleton getInstance() {
return DemoSingletonHolder.INSTANCE;
}
protected Object readResolve() {
return getInstance();
}
}
当不涉及序列化时,复杂对象也不需要延迟加载
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {}
}
*** 已编辑:在 @Holger 评论后添加关于序列化的内容
public class DemoSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private DemoSingleton() {
// private constructor
}
private static class DemoSingletonHolder {
public static final DemoSingleton INSTANCE = new DemoSingleton();
}
public static DemoSingleton getInstance() {
return DemoSingletonHolder.INSTANCE;
}
protected Object readResolve() {
return getInstance();
}
private int i = 10;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
public class DemoSingleton implements Serializable {
private volatile static DemoSingleton instance = null;
public static DemoSingleton getInstance() {
if (instance == null) {
instance = new DemoSingleton();
}
return instance;
}
private int i = 10;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
最佳答案
不清楚为什么您认为enum
类型没有延迟初始化。与其他类类型没有区别:
public class InitializationExample {
public static void main(String[] args) {
System.out.println("demonstrating lazy initialization");
System.out.println("accessing non-enum singleton");
Object o = Singleton.INSTANCE;
System.out.println("accessing the enum singleton");
Object p = SingletonEnum.INSTANCE;
System.out.println("q.e.d.");
}
}
public enum SingletonEnum {
INSTANCE;
private SingletonEnum() {
System.out.println("SingletonEnum initialized");
}
}
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {
System.out.println("Singleton initialized");
}
}
demonstrating lazy initialization
accessing non-enum singleton
Singleton initialized
accessing the enum singleton
SingletonEnum initialized
q.e.d.
由于这两种情况都已经存在惰性,因此没有理由像可序列化单例示例中那样使用嵌套类型。您仍然可以使用更简单的形式
public class SerializableSingleton implements Serializable {
public static final SerializableSingleton INSTANCE = new SerializableSingleton();
private static final long serialVersionUID = 1L;
private SerializableSingleton() {
System.out.println("SerializableSingleton initialized");
}
protected Object readResolve() {
return INSTANCE;
}
}
与enum
的区别在于字段确实被序列化,但这样做是没有意义的,因为反序列化后,重建的对象将被当前运行时的单例实例替换。这就是 readResolve()
方法的用途。
这是一个语义问题,因为可以有任意数量的不同序列化版本,但只有一个实际对象,否则它就不再是单例了。
只是为了完整性,
public class SerializableSingleton implements Serializable {
public static final SerializableSingleton INSTANCE = new SerializableSingleton();
private static final long serialVersionUID = 1L;
int value;
private SerializableSingleton() {
System.out.println("SerializableSingleton initialized");
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
protected Object readResolve() {
System.out.println("replacing "+this+" with "+INSTANCE);
return INSTANCE;
}
public String toString() {
return "SerializableSingleton{" + "value=" + value + '}';
}
}
SerializableSingleton single = SerializableSingleton.INSTANCE;
single.setValue(42);
byte[] data;
try(ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(single);
oos.flush();
data = baos.toByteArray();
}
single.setValue(100);
try(ByteArrayInputStream baos = new ByteArrayInputStream(data);
ObjectInputStream oos = new ObjectInputStream(baos)) {
Object deserialized = oos.readObject();
System.out.println(deserialized == single);
System.out.println(((SerializableSingleton)deserialized).getValue());
}
SerializableSingleton initialized
replacing SerializableSingleton{value=42} with SerializableSingleton{value=100}
true
100
因此,在这里使用普通类没有行为优势。存储字段与单例性质相矛盾,在最好的情况,这些值没有任何效果,反序列化的对象会被实际的运行时对象替换,就像enum
常量一样首先反序列化为规范对象。
此外,延迟初始化也没有区别。因此非枚举类需要编写更多代码才能得到更好的结果。
事实上,readResolve()
机制需要先反序列化一个对象,然后才能将其替换为实际结果对象,这不仅效率低下,而且暂时违反了单例不变量,而且这种违反并不总是在流程结束时得到彻底解决。
这为序列化黑客提供了可能性:
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class TestSer {
public static void main(String[] args) throws IOException, ClassNotFoundException {
SerializableSingleton singleton = SerializableSingleton.INSTANCE;
String data = "’\0\5sr\0\25SerializableSingleton\0\0\0\0\0\0\0\1\2\0\1L\0\1at\0\10"
+ "LSneaky;xpsr\0\6SneakyOÎæJ&r\234©\2\0\1L\0\1rt\0\27LSerializableSingleton;"
+ "xpq\0~\0\2";
try(ByteArrayInputStream baos=new ByteArrayInputStream(data.getBytes("iso-8859-1"));
ObjectInputStream oos = new ObjectInputStream(baos)) {
SerializableSingleton official = (SerializableSingleton)oos.readObject();
System.out.println(official+"\t"+(official == singleton));
Object inofficial = Sneaky.instance.r;
System.out.println(inofficial+"\t"+(inofficial == singleton));
}
}
}
class Sneaky implements Serializable {
static Sneaky instance;
SerializableSingleton r;
Sneaky(SerializableSingleton s) {
r = s;
}
private Object readResolve() {
return instance = this;
}
}
SerializableSingleton initialized
replacing SerializableSingleton@bebdb06 with SerializableSingleton@7a4f0f29
SerializableSingleton@7a4f0f29 true
SerializableSingleton@bebdb06 false
如所示,readObject()
按预期返回规范实例,但我们的 Sneaky
类提供了对“单例”的第二个实例的访问,该实例本应具有暂时性。
之所以有效,正是因为字段被序列化和反序列化。特殊构造的(偷偷摸摸的)流数据包含一个实际上在单例中不存在的字段,但由于 serialVersionUID
匹配,ObjectInputStream
将接受该数据,恢复对象然后删除它,因为没有字段可以存储它。但此时,Sneaky
实例已经通过循环引用获得了单例并记住了它。
对enum
类型的特殊处理使它们免受此类攻击。
关于java - 通过枚举实现的单例在模块化趋势中仍然值得(即 Java 9+ 模块化和 Jigsaw 项目),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60436876/