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)
Odpowiedzi:
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 implementacjaFoo1.getInstance()
może ewoluować, na przykład zwracając jedną instancję na wątek.Z
Foo2.INSTANCE
Publicznie 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
Foo2
rozszerza super klasęEnum<Foo2>
. Zwykle chcemy unikać superklas; zwłaszcza w tym przypadku narzuconaFoo2
superklasa nie ma nic wspólnego z tym, coFoo2
powinno być. Jest to zanieczyszczenie dla hierarchii typów naszej aplikacji. Jeśli naprawdę chcemy superklasy, zwykle jest to klasa aplikacji, ale nie możemy,Foo2
superklasa jest naprawiona.Foo2
dziedziczy niektóre zabawne metody instancjiname(), cardinal(), compareTo(Foo2)
, które są po prostu mylące dlaFoo2
użytkowników.Foo2
nie może mieć własnejname()
metody, nawet jeśli ta metoda jest pożądana wFoo2
interfejsie.Foo2
zawiera także kilka zabawnych metod statycznychco 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ć.
Foo2
automatycznie stosuje uproszczoną strategię serializacji / deserializacji. To tylko wypadek, który czeka. Jeśli mamy drzewo danych koncepcyjnie odnoszące się do zmiennej stanuFoo2
w VM1 w t1, poprzez serializację / deserializację wartość staje się inną wartością - wartością tej samej zmiennejFoo2
w VM2 w t2, tworząc trudny do wykrycia błąd. Ten błąd nie przydarzy się niemożliwym do znalezienia wFoo1
ciszy.Ograniczenia kodowania
Są rzeczy, które można zrobić w normalnych klasach, ale zabronione w
enum
klasach. 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ą.
źródło
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…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)
Teraz uruchom ten test, upewniając się, że nie ma kopii powyższego wyliczenia na ścieżce klasy:
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.
źródło
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:
źródło