【问题标题】:Is singleton implemented by enum still worth in modularity thrends (i.e. Java 9+ Modularity and Jigsaw Project)枚举实现的单例在模块化趋势中是否仍然值得(即 Java 9+ 模块化和 Jigsaw 项目)
【发布时间】:2020-02-27 15:44:12
【问题描述】:

我的直截了当的问题是:由于反射现在受到限制,考虑将 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 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;
    }
}

【问题讨论】:

  • “将枚举用于单例的主要优点是防止反射风险”但是,如果您将单例反序列化两次,这意味着什么?你怎么能停止这样做,并以多个实例结束?
  • 据我了解,序列化场景中的枚举毫无价值(我在上面写了“......当序列化枚举时,字段变量没有被序列化......”)。假设我是对的,你的问题在这里不适用,因为它不是枚举的 PRO
  • 我的观点是可变单例与序列化不兼容,因为您无法协调单例的当前状态与反序列化时加载的状态之间的差异。要么踩住当前状态(对于不希望它改变的人感到惊讶),放弃反序列化状态(为什么还要序列化状态),或者创建另一个实例(它不是单例)。
  • 首先,enum 类型和其他单例示例一样懒惰。没有区别。此外,您的可序列化单例示例毫无意义。它将反序列化存储的值,然后将反序列化的对象替换为已经存在的对象,就像 enum 一开始会做的那样。除了临时替换是脆弱的,你的“单身”有两个对象存在。
  • 在调用readResolve() 时,您有两个DemoSingleton 实例,一个调用readResolve(),另一个存储在DemoSingletonHolder.INSTANCE 中。这本身就是单例不变量的矛盾。正如@AndyTurner 所说,无论您将使用谁的对象状态,您都会打破某人的期望。更不用说如果两个线程同时反序列化两个不同版本的DemoSingleton 状态会发生什么。

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


【解决方案1】:

不清楚为什么你认为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模块化封装不再需要枚举实现所修复的反射问题”。老实说,根据我在上一小时学到的知识,我认为 Singleton 枚举方法甚至比其他方法更好。谢谢
猜你喜欢
  • 2021-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多