Jakie są wady implementacji singletonu z enum Java?

14

Tradycyjnie singleton jest zwykle implementowany jako

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Dzięki enum Java, możemy zaimplementować singleton jako

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

Jak niesamowita jest druga wersja, czy są w niej jakieś wady?

(Myślałem o tym, a ja odpowiem na własne pytanie; mam nadzieję, że masz lepsze odpowiedzi)

niezaprzeczalny
źródło
16
Minusem jest to, że jest to singleton. Całkowicie przereklamowany ( kaszel ) „wzór”
Thomas Eding,

Odpowiedzi:

32

Niektóre problemy z singlami enum:

Zaangażowanie w strategię wdrażania

Zazwyczaj „singleton” odnosi się do strategii implementacji, a nie specyfikacji API. Bardzo rzadko Foo1.getInstance()publicznie deklaruje, że zawsze zwróci tę samą instancję. W razie potrzeby implementacja Foo1.getInstance()może ewoluować, na przykład zwracając jedną instancję na wątek.

Z Foo2.INSTANCEPublicznie oświadczam, że ten przypadek jest instancji i nie ma szans, by to zmienić. Strategia wdrażania posiadania pojedynczej instancji jest ujawniona i zobowiązana.

Ten problem nie jest paraliżujący. Na przykład Foo2.INSTANCE.doo()może polegać na lokalnym obiekcie pomocniczym wątku, aby skutecznie mieć instancję dla wątku.

Rozszerzanie klasy Enum

Foo2rozszerza super klasę Enum<Foo2>. Zwykle chcemy unikać superklas; zwłaszcza w tym przypadku narzucona Foo2superklasa nie ma nic wspólnego z tym, co Foo2powinno być. Jest to zanieczyszczenie dla hierarchii typów naszej aplikacji. Jeśli naprawdę chcemy superklasy, zwykle jest to klasa aplikacji, ale nie możemy, Foo2superklasa jest naprawiona.

Foo2dziedziczy niektóre zabawne metody instancji name(), cardinal(), compareTo(Foo2), które są po prostu mylące dla Foo2użytkowników. Foo2nie może mieć własnej name()metody, nawet jeśli ta metoda jest pożądana w Foo2interfejsie.

Foo2 zawiera także kilka zabawnych metod statycznych

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

co wydaje się bezsensowne dla użytkowników. Singleton zazwyczaj nie powinien mieć metod statycznych pulbic (inaczej niż getInstance())

Serializacja

Singletony są bardzo częste. Te singletony zasadniczo nie powinny być serializowane. Nie mogę wymyślić żadnego realistycznego przykładu, w którym sensowne byłoby transportowanie stanowego singletonu z jednej maszyny wirtualnej na inną maszynę wirtualną; singleton oznacza „unikalny w obrębie maszyny wirtualnej”, a nie „unikalny we wszechświecie”.

Jeśli serializacja naprawdę ma sens dla stanowego singletonu, singleton powinien jawnie i precyzyjnie określić, co to znaczy deserializować singleton na innej maszynie wirtualnej, gdzie singleton tego samego typu może już istnieć.

Foo2automatycznie stosuje uproszczoną strategię serializacji / deserializacji. To tylko wypadek, który czeka. Jeśli mamy drzewo danych koncepcyjnie odnoszące się do zmiennej stanu Foo2w VM1 w t1, poprzez serializację / deserializację wartość staje się inną wartością - wartością tej samej zmiennej Foo2w VM2 w t2, tworząc trudny do wykrycia błąd. Ten błąd nie przydarzy się niemożliwym do znalezienia w Foo1ciszy.

Ograniczenia kodowania

Są rzeczy, które można zrobić w normalnych klasach, ale zabronione w enumklasach. Na przykład dostęp do pola statycznego w konstruktorze. Programista musi być bardziej ostrożny, ponieważ pracuje w specjalnej klasie.

Wniosek

Piggybacking na enum oszczędzamy 2 linie kodu; ale cena jest zbyt wysoka, musimy dźwigać wszystkie bagaże i ograniczenia wyliczeń, mimowolnie dziedziczymy „cechy” wyliczenia, które mają niezamierzone konsekwencje. Jedyną rzekomą zaletą - automatyczna serializacja - okazuje się być wadą.

niezaprzeczalny
źródło
2
-1: Twoja dyskusja na temat serializacji jest nieprawidłowa. Mechanizm nie jest uproszczony, ponieważ wyliczenia są traktowane bardzo inaczej niż zwykłe instancje podczas deserializacji. Opisany problem nie występuje, ponieważ faktyczny mechanizm deserializacji nie modyfikuje „zmiennej stanu”.
scarfridge
zobacz ten przykład zamieszania: coderanch.com/t/498782/java/java/...
bezsporny
2
W rzeczywistości połączona dyskusja podkreśla mój punkt widzenia. Pozwól mi wyjaśnić, co rozumiałem jako problem, który Twoim zdaniem istnieje. Niektóre obiekty A odnoszą się do drugiego obiektu B. Instancja singletona S również odwołuje się do B. Teraz deserializujemy wcześniej zserializowaną instancję singletonu opartego na wyliczeniu (który w momencie serializacji oznaczał B '! = B). W rzeczywistości dzieje się tak, że A i S odnoszą się do B, ponieważ B 'nie będą serializowane. Myślałem, że chcesz wyrazić, że A i S nie odnoszą się już do tego samego obiektu.
scarfridge
1
Może tak naprawdę nie mówimy o tym samym problemie?
scarfridge
1
@Kevin Krumwiede: Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);np. Naprawdę dobrym przykładem byłby :; Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);^), przetestowany pod Javą 7 i Javą 8…
Holger
6

Instancja wyliczenia zależy od modułu ładującego klasy. tzn. jeśli masz moduł ładujący drugiej klasy, który nie ma modułu ładującego pierwszej klasy jako element nadrzędny ładujący tę samą klasę enum, możesz uzyskać wiele instancji w pamięci.


Próbka kodu

Utwórz następujący wyliczenie i umieść sam plik .class w słoju. (oczywiście słoik będzie miał poprawną strukturę pakietu / folderu)

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

Teraz uruchom ten test, upewniając się, że nie ma kopii powyższego wyliczenia na ścieżce klasy:

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

A teraz mamy dwa obiekty reprezentujące „tę samą” wartość wyliczenia.

Zgadzam się, że jest to rzadki i przemyślany przypadek narożny, i prawie zawsze można zastosować wyliczenie dla singletona Java. Robię to sam. Ale pytanie o potencjalne wady i ta uwaga są warte poznania.

Mad G.
źródło
Czy możesz znaleźć jakieś odniesienia do tego problemu?
Zredagowałem i poprawiłem swoją pierwotną odpowiedź o przykładowy kod. Mamy nadzieję, że pomogą one zilustrować tę kwestię i odpowiedzieć również na zapytanie MichaelT.
Mad G,
@MichaelT: Mam nadzieję, że to odpowiada na twoje pytanie :-)
Mad G
Tak więc, jeśli powodem użycia enum (zamiast klasy) dla singletonu było tylko jego bezpieczeństwo, nie ma teraz żadnego powodu ... Doskonale, +1
Gangnus
1
Czy „tradycyjna” implementacja singletonu będzie działać zgodnie z oczekiwaniami, nawet w przypadku dwóch różnych programów ładujących klasy?
Ron Klein
3

wzorzec wyliczenia nie może być użyty dla żadnej klasy, która zgłasza wyjątek w konstruktorze. Jeśli jest to wymagane, użyj fabryki:

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;
    }        
}
użytkownik3158036
źródło