Які недоліки в застосуванні синглтона з перерахунком 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

Деякі проблеми з перерахунком синглів:

Прихильність до стратегії впровадження

Як правило, "одиночний" відноситься до стратегії реалізації, а не до специфікації 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/…
незаперечний

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…
Holger

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

І тепер у нас є два об'єкти, що представляють "те саме" значення перерахунку.

Я погоджуюсь, що це рідкісний і надуманий кутовий випадок, і майже завжди перерахунок може бути використаний для синглу java. Я роблю це сам. Але питання про потенційні недоліки, і про цю ноту обережності варто знати.


Чи можете ви знайти якісь посилання на це питання?

Зараз я відредагував і доповнив свою початкову відповідь прикладом коду. Сподіваємось, вони допоможуть проілюструвати точку та відповісти на запит MichaelT.
Mad G

@MichaelT: Я сподіваюся, що відповість на ваше запитання :-)
Mad G

Отже, якщо причиною використання enum (замість класу) для одиночної була лише його безпека, то зараз немає жодної причини для цього ... Відмінно, +1
Gangnus

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

3

enum pattern не може бути використаний для будь-якого класу, який би кинув виняток у конструкторі. Якщо це потрібно, використовуйте фабрику:

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.