Каковы недостатки реализации синглтона с перечислением Java?


14

Традиционно синглтон обычно реализуется как

public class Foo1
{
    private static final Foo1 INSTANCE = new Foo1();

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

С помощью перечисления Java мы можем реализовать синглтон как

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

Как бы ни была хороша вторая версия, есть ли у нее недостатки?

(Я подумал об этом, и я отвечу на свой вопрос; надеюсь, у вас есть лучшие ответы)


16
Недостатком является то, что это синглтон. Полностью переоцененный ( кашляющий ) "образец"
Томас Эдинг

Ответы:


32

Некоторые проблемы с синглетонами enum:

Приверженность стратегии реализации

Как правило, «синглтон» относится к стратегии реализации, а не к спецификации API. Очень редко Foo1.getInstance()публично заявляют, что он всегда будет возвращать один и тот же экземпляр. При необходимости реализация Foo1.getInstance()может развиваться, например, для возврата одного экземпляра на поток.

С Foo2.INSTANCEмы публично заявляем , что данный экземпляр экземпляр, и нет никаких шансов , чтобы изменить это. Стратегия реализации, состоящая в том, чтобы иметь единственный экземпляр, является открытой и преданной.

Эта проблема не наносит вред. Например, Foo2.INSTANCE.doo()может полагаться на локальный вспомогательный объект потока, чтобы эффективно иметь экземпляр для каждого потока.

Расширение класса Enum

Foo2расширяет супер класс Enum<Foo2>. Мы обычно хотим избегать супер-классов; особенно в этом случае, вынужденный суперкласс Foo2не имеет ничего общего с тем, что Foo2должно быть. Это загрязнение иерархии типов нашего приложения. Если мы действительно хотим суперкласс, обычно это класс приложения, но мы не можем, Foo2суперкласс исправлен.

Foo2наследует некоторые забавные методы экземпляра name(), cardinal(), compareTo(Foo2), которые просто сбивают с толку Foo2пользователей России. Foo2не может иметь свой собственный name()метод, даже если этот метод желателен в Foo2интерфейсе.

Foo2 также содержит несколько забавных статических методов

    public static Foo2[] values() { ... }
    public static Foo2 valueOf(String name) { ... }
    public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name)

который кажется бессмысленным для пользователей. У синглтона обычно не должно быть пульсирующих статических методов в любом случае (кроме getInstance())

Сериализуемость

Синглтонам очень свойственно быть с состоянием. Эти синглтоны обычно не должны быть сериализуемыми. Я не могу вспомнить ни одного реалистичного примера, когда имеет смысл переносить синглтон с состоянием с одной виртуальной машины на другую; «Синглтон» означает «уникальный внутри ВМ», а не «уникальный во вселенной».

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

Foo2автоматически фиксирует упрощенную стратегию сериализации / десериализации. Это просто несчастный случай, ожидающий случиться. Если у нас есть дерево данных, концептуально ссылающееся на переменную состояния Foo2в VM1 в момент времени t1, посредством сериализации / десериализации значение становится другим значением - значением той же переменной Foo2в VM2 в момент времени t2, что создает трудно обнаруживаемую ошибку. Эта ошибка не произойдет с не сериализуемым Foo1молча.

Ограничения кодирования

Есть вещи, которые можно делать в обычных классах, но запрещать в enumклассах. Например, доступ к статическому полю в конструкторе. Программист должен быть более осторожным, поскольку он работает в специальном классе.

Вывод

Используя перечисление enum, мы сохраняем 2 строки кода; но цена слишком высока, мы должны нести все мешки и ограничения перечислений, мы непреднамеренно наследуем «особенности» перечислений, которые имеют непредвиденные последствия. Единственное заявленное преимущество - автоматическая сериализуемость - оказывается недостатком.


2
-1: Ваше обсуждение сериализации неверно. Механизм не является упрощенным, поскольку перечисления обрабатываются очень по-разному, чем обычные экземпляры во время десериализации. Описанная проблема не возникает, поскольку фактический механизм десериализации не изменяет «переменную состояния».
шарфридж

см. этот пример путаницы: coderanch.com/t/498782/java/java/…
неопровержимый 14.12.12

2
На самом деле связанная дискуссия подчеркивает мою точку зрения. Позвольте мне объяснить, что я понял как проблему, о которой вы заявляете, что существует. Некоторый объект A ссылается на второй объект B. Одиночный экземпляр S также ссылается на B. Теперь мы десериализовали ранее сериализованный экземпляр синглтона, основанного на перечислении (который во время сериализации ссылался на B '! = B). На самом деле происходит то, что A и S ссылаются на B, потому что B 'не будет сериализовано. Я думал, что вы хотели выразить, что A и S больше не ссылаются на один и тот же объект.
шарфридж

1
Может быть, мы на самом деле не говорим об одной и той же проблеме?
шарфридж

1
@Kevin Krumwiede: Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);например, действительно хороший пример :; Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);^), протестирован под Java 7 и Java 8…
Хольгер

6

Экземпляр enum зависит от загрузчика классов. т. е. если у вас есть загрузчик второго класса, у которого загрузчик первого класса не является родительским, загружающим один и тот же класс enum, вы можете получить несколько экземпляров в памяти.


Пример кода

Создайте следующее перечисление и поместите его файл .class в jar-файл. (конечно, фляга будет иметь правильную структуру пакета / папки)

package mad;
public enum Side {
  RIGHT, LEFT;
}

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

@Test
public void testEnums() throws Exception
{
    final ClassLoader root = MadTest.class.getClassLoader();

    final File jar = new File("path to jar"); // Edit path
    assertTrue(jar.exists());
    assertTrue(jar.isFile());

    final URL[] urls = new URL[] { jar.toURI().toURL() };
    final ClassLoader cl1 = new URLClassLoader(urls, root);
    final ClassLoader cl2 = new URLClassLoader(urls, root);

    final Class<?> sideClass1 = cl1.loadClass("mad.Side");
    final Class<?> sideClass2 = cl2.loadClass("mad.Side");

    assertNotSame(sideClass1, sideClass2);

    assertTrue(sideClass1.isEnum());
    assertTrue(sideClass2.isEnum());
    final Field f1 = sideClass1.getField("RIGHT");
    final Field f2 = sideClass2.getField("RIGHT");
    assertTrue(f1.isEnumConstant());
    assertTrue(f2.isEnumConstant());

    final Object right1 = f1.get(null);
    final Object right2 = f2.get(null);
    assertNotSame(right1, right2);
}

И теперь у нас есть два объекта, представляющих одно и то же значение перечисления.

Я согласен, что это редкий и надуманный угловой случай, и почти всегда enum может использоваться для java-синглтона. Я делаю это сам. Но вопрос, который задают о потенциальных недостатках, и эта предостережение стоит знать.


Можете ли вы найти какие-либо ссылки на эту проблему?

Теперь я отредактировал и улучшил свой первоначальный ответ с помощью примера кода. Надеюсь, они помогут проиллюстрировать это и ответят на вопрос MichaelT.
Безумный G

@MichaelT: Я надеюсь, что это отвечает на ваш вопрос :-)
Безумный G

Итак, если причиной использования enum (а не class) для синглтона была только его безопасность, то сейчас нет никаких причин для этого ... Отлично, +1
Gangnus

1
Будет ли "традиционная" реализация синглтона работать так, как ожидается, даже с двумя различными загрузчиками классов?
Рон Кляйн

3

Шаблон enum не может быть использован для любого класса, который может вызвать исключение в конструкторе. Если это требуется, используйте фабрику:

class Elvis {
    private static Elvis self = null;
    private int age;

    private Elvis() throws Exception {
        ...
    }

    public synchronized Elvis getInstance() throws Exception {
        return self != null ? self : (self = new Elvis());
    }

    public int getAge() {
        return age;
    }        
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.