由枚举实现的单例仍然值得在模块化流行中使用(例如Java 9+模块化和Jigsaw Project)

问题描述 投票:1回答:1

我的直接问题是:由于反射现在受到限制,所以考虑将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的可访问性规则现在仅限制对公共元素(类型,方法,字段)的访问”,以及由枚举解决的反思问题,我们可能想知道为什么仍然代码单例作为枚举。

尽管简单,但是在枚举序列时,字段变量不会被序列化。最重要的是,枚举不支持延迟加载。

总结起来,假设我上面没有说过任何愚蠢的事情,因为将enum用于单例的主要优点是可以防止反射风险,因此我得出的结论是,将单例编码为枚举不再比a更好。像这样的静态方法的简单实现:

需要序列化时

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;
    }
}
java enums singleton java-module jigsaw
1个回答
0
投票

尚不清楚为什么会认为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("SingletonEnum initialized");
    }
}
demonstrating lazy initialization
accessing non-enum singleton
SingletonEnum 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类型的特殊处理使它们不受这种攻击。

© www.soinside.com 2019 - 2024. All rights reserved.