枚举实现的单例是否仍然值得模块化趋势(即 Java 9+ 模块化和 Jigsaw 项目)

Is singleton implemented by enum still worth in modularity thrends (i.e. Java 9+ Modularity and Jigsaw Project)

我的直截了当的问题是:考虑到单例实现的枚举是否仍然有意义,因为反射现在受到限制?

通过单例实现抛出枚举,我的意思是像这样的实现:

public enum SingletonEnum {
    INSTANCE;
    int value;
    public int getValue() {
        return value;
    }
    public void setValue(int value) {
        this.value = value;
    }
}

如果我们对比 中提到的模块化基本思想“... 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 类型没有延迟初始化。与其他 class 类型没有区别:

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

所以在这里使用普通的 class 没有行为上的优势。存储字段与单例性质相矛盾,在最好的情况下,这些值没有效果,反序列化对象被实际运行时对象替换,就像 enum 常量一样首先反序列化为规范对象。

此外,延迟初始化也没有区别。因此,非枚举 class 需要编写更多代码才能获得更好的结果。

事实上 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 = "’[=16=]sr[=16=]SerializableSingleton[=16=][=16=][=16=][=16=][=16=][=16=][=16=][=16=]L[=16=]at[=16=]"
            + "LSneaky;xpsr[=16=]SneakyOÎæJ&r4©[=16=]L[=16=]rt[=16=]LSerializableSingleton;"
            + "xpq[=16=]~[=16=]";
        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() returns 是预期的规范实例,但是我们的 Sneaky class 提供了对“单例”的第二个实例的访问应该是临时性质的。

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

enum 类型的特殊待遇使他们免疫此类攻击。