Статьи

Java Singletons с использованием Enum

Синглтон — это класс, который должен иметь только один экземпляр на JVM. Один и тот же экземпляр класса singleton повторно используется несколькими потоками. Чаще всего мы используем синглеты для представления конфигураций системы и оконных менеджеров, поскольку эти экземпляры должны быть общими для всех потоков и объектов в JVM.

Традиционные методы изготовления синглетонов

Существует несколько популярных способов изготовления синглетонов.

Метод 1: Синглтон с публичным статическим финальным полем

public class Singleton {

    public static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

}

Метод 2: Синглтон с публичной статической фабрикой Метод

public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance(){
        return INSTANCE;
    }

}

Метод 3: Singleton с ленивой инициализацией

public class Singleton {

    private static Singleton INSTANCE = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

}

Плюсы и минусы вышеперечисленных методов

Все вышеперечисленные методы обеспечивают ненасыщенность (невозможность создания экземпляров) с помощью закрытого конструктора. Здесь мы не можем избежать создания частного конструктора, даже если нам нечего делать внутри него. Потому что если мы это сделаем, то будет создан неявный конструктор по умолчанию без параметров с тем же модификатором доступа, что и у класса. Например, если класс объявлен как открытый, то конструктор по умолчанию является открытым; если класс объявлен защищенным, то конструктор по умолчанию защищен (обратитесь к  документации Oracle  для более подробной информации).

Сравнивая вышеупомянутые методы, первые два метода вообще не имеют разницы в производительности. Способ 1 понятнее и проще. Небольшое преимущество метода 2 заключается в том, что позже вы можете сделать класс не одноэлементным без изменения API. Вы можете сделать это, изменив реализацию фабричного метода, чтобы создать новый экземпляр для каждого вызова вместо того, чтобы возвращать тот же экземпляр, как показано ниже.

public static Singleton getInstance() {
    return new Singleton ();
}

Статические поля инициализируются во время загрузки класса. Поэтому в обоих методах 1 и 2 одиночные экземпляры создаются даже в том случае, если мы не используем их во время выполнения. Это не проблема, если одноэлементный объект не слишком большой и создание экземпляра не слишком дорого. Метод 3 позволяет избежать этой проблемы с отложенной инициализацией. В методе 3 экземпляр создается при первом обращении к одноэлементному объекту. Детальная синхронизация используется для обеспечения того, чтобы в нескольких параллельных потоках создавалось не более одного объекта. 

Все вышеперечисленные методы работают нормально, пока вы не выполняете сериализацию и десериализацию с одноэлементным классом. Давайте еще раз подумаем: как мы достигли одноэлементного поведения в описанных выше методах? Это было сделано, сделав конструктор частным и сделав конструктор недоступным для создания новых экземпляров класса. Но нет ли других способов создать экземпляр класса, кроме конструктора? Ответ — нет. Есть несколько других продвинутых методов.

  1. Сериализация и десериализация
  2. отражение

Проблемы с сериализацией и десериализацией

Чтобы сериализовать вышеупомянутые одноэлементные классы, мы должны реализовать эти классы с помощью  Serializable интерфейса. Но этого недостаточно. При десериализации класса создаются новые экземпляры. Теперь не имеет значения, является ли конструктор частным или нет. Теперь внутри JVM может быть несколько экземпляров одного и того же синглтон-класса, что нарушает свойство синглтона. 

public class SerializeDemo implements Serializable {

    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.setValue(1);

        // Serialize
        try {
            FileOutputStream fileOut = new FileOutputStream("out.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(singleton);
            out.close();
            fileOut.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        singleton.setValue(2);

        // Deserialize
        Singleton singleton2 = null;
        try {
            FileInputStream fileIn = new FileInputStream("out.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            singleton2 = (Singleton) in.readObject();
            in.close();
            fileIn.close();
        } catch (IOException i) {
            i.printStackTrace();
        } catch (ClassNotFoundException c) {
            System.out.println("singletons.SingletonEnum class not found");
            c.printStackTrace();
        }

        if (singleton == singleton2) {
            System.out.println("Two objects are same");
        } else {
            System.out.println("Two objects are not same");
        }

        System.out.println(singleton.getValue());
        System.out.println(singleton2.getValue());

    }

}

Выход для вышеуказанного кода:

Two objects are not same
2
1

Здесь singleton и singleton2 — это два разных экземпляра, имеющие два разных значения в качестве переменных поля. Это нарушает свойство синглтона. Решение состоит в том, что мы должны реализовать   метод readResolve , который вызывается при подготовке десериализованного объекта перед возвратом его вызывающей стороне. Решение заключается в следующем.

public class Singleton implements Serializable{

    public static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    protected Object readResolve() {
        return INSTANCE;
    }

}

Теперь вывод для приведенного выше кода будет:

Two objects are same
2
2

Теперь свойство singleton сохраняется, и в JVM существует только один экземпляр класса singleton.

Проблемы с отражением

Опытный пользователь может изменить модификатор личного доступа конструктора на что угодно во время выполнения, используя отражение. Если это произойдет, наш единственный механизм для разрывов неинстанцируемости. Посмотрим, как это можно сделать.

public class ReflectionDemo {

    public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.INSTANCE;

        Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
        constructor.setAccessible(true);

        Singleton singleton2 = (Singleton) constructor.newInstance();

        if (singleton == singleton2) {
            System.out.println("Two objects are same");
        } else {
            System.out.println("Two objects are not same");
        }

        singleton.setValue(1);
        singleton2.setValue(2);

        System.out.println(singleton.getValue());
        System.out.println(singleton2.getValue());

    }
}

Вывод:

Two objects are not same
1
2

Таким образом, недоступный приватный конструктор становится доступным, и вся идея сделать класс одиночным разрушается.

Создание синглетонов с помощью Enum на Java

Все вышеперечисленные проблемы могут быть легко решены с помощью типа enum для создания синглетонов.

Синглтон с Enum

public enum Singleton {
    INSTANCE;
}

Три вышеупомянутые строки составляют единый без обсуждаемых проблем. Поскольку перечисления по своей природе сериализуемы, нам не нужно реализовывать их с помощью сериализуемого интерфейса. Проблема отражения также не существует. Поэтому на 100% гарантируется, что в JVM присутствует только один экземпляр синглтона. Таким образом, этот метод рекомендуется как лучший способ создания синглетонов в Java.

Как пользоваться

public enum SingletonEnum {
    INSTANCE;

    int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

Реализация основного класса:

public class EnumDemo {

    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.INSTANCE;

        System.out.println(singleton.getValue());
        singleton.setValue(2);
        System.out.println(singleton.getValue());
    }
}

Здесь следует помнить, что при сериализации перечисления переменные поля не сериализуются. Например, если мы сериализовали и десериализовали  SingletonEnum класс, мы потеряем значение  int value поля (обратитесь к  документации Oracle  для получения дополнительной информации о сериализации enum).

Весь исходный код, связанный с этим постом, можно найти здесь .