java - 通过枚举实现的单例在模块化趋势中仍然值得(即 Java 9+ 模块化和 Jigsaw 项目)

标签 java enums singleton java-module java-platform-module-system

我的直接问题是:既然反射现在受到限制,考虑使用 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

Also on Ideone

如所示,readObject() 按预期返回规范实例,但我们的 Sneaky 类提供了对“单例”的第二个实例的访问,该实例本应具有暂时性。

之所以有效,正是因为字段被序列化和反序列化。特殊构造的(偷偷摸摸的)流数据包含一个实际上在单例中不存在的字段,但由于 serialVersionUID 匹配,ObjectInputStream 将接受该数据,恢复对象然后删除它,因为没有字段可以存储它。但此时,Sneaky 实例已经通过循环引用获得了单例并记住了它。

enum类型的特殊处理使它们免受此类攻击。

关于java - 通过枚举实现的单例在模块化趋势中仍然值得(即 Java 9+ 模块化和 Jigsaw 项目),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60436876/

相关文章:

java - 获取 jcombobox 选中的项目

c# - 如何设置枚举标志的所有位

java - IntelliJ 无法识别 Java 枚举

Java : Singleton class instances in a Web based Application

java - 如何仅使用主键将引用的持久类序列化为 JSON

java - 如何使用Java创建Azure服务总线队列?

java - 使用自定义标记 (taglib) 在 JSP 上提示 Eclipse 代码

java - 如何在 Java 中声明一个 Class<?> 对象,它是一个枚举和一个接口(interface)

ios - 常量的 getter 和 setter ?

java - 在java中使用反射调用Singleton类中的方法