Реализован ли синглтон с помощью enum, все еще стоящий в модулярных цепочках (например, Java 9+ Modularity и Jigsaw Project) - PullRequest
2 голосов
/ 27 февраля 2020

Мой прямой вопрос: имеет ли смысл рассматривать Enum для одноэлементной реализации, поскольку Reflection теперь ограничен?

Под одноэлементной реализацией throw enum я имею в виду некоторую реализацию, например:

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

Если мы противопоставим базовую c идею модульности, упомянутую в ответе , касающуюся доступа к пакету области действия"... Правила доступности Jigsaw теперь ограничивают доступ только к элементам publi c (типы, методы, поля) "и проблема рефлексии, исправленная enum, мы можем задаться вопросом, почему все еще кодируют синглтон как enum.

Несмотря на свою простоту, при сериализации enum переменные поля не сериализуются. Кроме того, перечисления не поддерживают отложенную загрузку.

Подводя итог, предполагая, что я не сказал ничего дурацкого sh выше, поскольку основным преимуществом использования enum для синглтона была защита от рисков отражения, я бы пришел к выводу, что кодирование синглтона как enum ничуть не лучше, чем простая реализация вокруг stati c, например:

Когда необходима сериализация

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;
    }
}

1 Ответ

4 голосов
/ 27 февраля 2020

Непонятно, почему вы думаете, что 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().

Это проблема семантики c, так как может быть произвольное количество различных сериализованных версий, но только один фактический объект, так как в противном случае это не было бы Singleton больше.

Просто для полноты,

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 десериализуется в канонический объект в первое место.

Кроме того, нет никакой разницы в отношении отложенной инициализации. Таким образом, класс non-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

Также на Ideone

Как продемонстрировано, readObject() возвращает канонический экземпляр, как и предполагалось, но наш класс Sneaky предоставляет доступ ко второму экземпляру «singleton», который должен был иметь временную природу.

Причина, по которой это работает, заключается именно в том, что поля сериализуются и десериализуются. Специально сконструированные (скрытые) данные потока содержат поле, которого на самом деле не существует в синглтоне, но, поскольку serialVersionUID совпадает, ObjectInputStream примет данные, восстановит объект и затем отбросит его, потому что нет поля для сохранить его. Но в это время экземпляр Sneaky уже получил в руки синглтон через ссылку cycli c и запоминает его.

Специальная обработка типов enum делает их невосприимчивыми к таким атакам.

...