Was sind die Nachteile der Implementierung eines Singleton mit Javas Aufzählung?


14

Traditionell wird ein Singleton normalerweise als implementiert

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Mit Javas Enum können wir einen Singleton als implementieren

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

Gibt es bei der 2. Version auch Nachteile?

(Ich habe darüber nachgedacht und beantworte meine eigene Frage. Hoffentlich hast du bessere Antworten.)


16
Der Nachteil ist, dass es ein Singleton ist. Ein total überbewertetes ( Husten ) "Muster"
Thomas Eding

Antworten:


32

Einige Probleme mit Enum Singletons:

Bekenntnis zu einer Umsetzungsstrategie

In der Regel bezieht sich "Singleton" auf eine Implementierungsstrategie und nicht auf eine API-Spezifikation. Es ist sehr selten Foo1.getInstance(), dass öffentlich deklariert wird, dass immer dieselbe Instanz zurückgegeben wird. Bei Bedarf kann die Implementierung vonFoo1.getInstance() so weiterentwickelt werden, dass eine Instanz pro Thread zurückgegeben wird.

Mit Foo2.INSTANCEuns erklären öffentlich , dass diese Instanz ist die Instanz ist, und es gibt keine Chance, das zu ändern. Die Implementierungsstrategie, eine einzelne Instanz zu haben, wird offengelegt und begangen.

Dieses Problem lähmt nicht. Beispielsweise Foo2.INSTANCE.doo()kann auf ein Thread-lokales Hilfsobjekt zurückgegriffen werden, um effektiv zu arbeiten eine Instanz pro Thread zu haben.

Erweiterung der Enum-Klasse

Foo2erweitert eine super klasse Enum<Foo2>. Wir wollen normalerweise Superklassen vermeiden; gerade in diesem Fall hat die aufgezwungene Foo2Superklasse nichts mit dem zu tun, was sein Foo2soll. Das ist eine Verschmutzung der Typenhierarchie unserer Anwendung. Wenn wir wirklich eine Superklasse wollen, ist es normalerweise eine Anwendungsklasse, aber wir können nicht, Foo2die Superklasse ist behoben.

Foo2erbt einige lustige Instanzmethoden wie name(), cardinal(), compareTo(Foo2), die die Foo2Benutzer nur verwirren . Foo2Ich kann keine eigene name()Methode haben, auch wenn diese Methode in Foo2der Benutzeroberfläche wünschenswert ist .

Foo2 enthält auch einige lustige statische Methoden

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

was für Benutzer unsinnig erscheint. Ein Singleton sollte normalerweise sowieso keine pulbischen statischen Methoden haben (außer der getInstance())

Serialisierbarkeit

Singletons sind sehr häufig staatlich. Diese Singletons sollten im Allgemeinen nicht serialisierbar sein. Ich kann mir kein realistisches Beispiel vorstellen, bei dem es sinnvoll ist, einen statusbehafteten Singleton von einer VM zu einer anderen zu transportieren. Ein Singleton bedeutet "einzigartig innerhalb einer VM", nicht "einzigartig im Universum".

Wenn die Serialisierung für ein statusbehaftetes Singleton wirklich Sinn macht, sollte das Singleton explizit und genau angeben, was es bedeutet, ein Singleton in einer anderen VM zu deserialisieren, in der möglicherweise bereits ein Singleton desselben Typs vorhanden ist.

Foo2legt automatisch eine vereinfachte Serialisierungs- / Deserialisierungsstrategie fest. Das ist nur ein Unfall, der darauf wartet, passiert zu werden. Wenn wir einen Foo2Datenbaum haben, der konzeptionell auf eine Zustandsvariable von in VM1 zu t1 verweist, wird der Wert durch Serialisierung / Deserialisierung zu einem anderen Wert - der Wert derselben Variablen von Foo2in VM2 zu t2, wodurch ein schwer zu erkennender Fehler entsteht. Dieser Fehler tritt nicht bei den unserializable aufFoo1 .

Einschränkungen der Codierung

Es gibt Dinge, die in normalen Klassen gemacht werden können, aber in enumKlassen verboten sind. Zum Beispiel auf ein statisches Feld im Konstruktor zugreifen. Der Programmierer muss vorsichtiger sein, da er in einer speziellen Klasse arbeitet.

Fazit

Durch Huckepack auf Enum sparen wir 2 Codezeilen; aber der Preis ist zu hoch, wir müssen alle Gepäckstücke und Beschränkungen von Aufzählungen tragen, wir erben versehentlich "Merkmale" von Aufzählungen, die unbeabsichtigte Konsequenzen haben. Der einzige vermeintliche Vorteil - die automatische Serialisierbarkeit - erweist sich als Nachteil.


2
-1: Ihre Diskussion über die Serialisierung ist falsch. Der Mechanismus ist nicht einfach, da Aufzählungen während der Deserialisierung sehr unterschiedlich zu regulären Instanzen behandelt werden. Das beschriebene Problem tritt nicht auf, da der tatsächliche Deserialisierungsmechanismus eine "Zustandsvariable" nicht ändert.
scarfridge

siehe dieses Beispiel der Verwirrung: coderanch.com/t/498782/java/java/…
unwiderlegbar

2
Eigentlich unterstreicht die verknüpfte Diskussion meinen Standpunkt. Lassen Sie mich erklären, was ich als das Problem verstanden habe, von dem Sie behaupten zu existieren. Einige Objekte A referenzieren ein zweites Objekt B. Die Singleton-Instanz S referenziert ebenfalls B. Jetzt deserialisieren wir eine zuvor serialisierte Instanz des enum-basierten Singleton (der zum Zeitpunkt der Serialisierung mit B '! = B bezeichnet war). Was tatsächlich passiert ist, dass A und S auf B verweisen, da B 'nicht serialisiert wird. Ich dachte, Sie wollten ausdrücken, dass A und S nicht mehr dasselbe Objekt referenzieren.
scarfridge

1
Vielleicht sprechen wir nicht wirklich über dasselbe Problem?
scarfridge

1
@ Kevin Krumwiede: Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);zB ein wirklich schönes Beispiel wäre :; Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);^), getestet unter Java 7 und Java 8…
Holger

6

Eine Enum-Instanz ist vom Klassenladeprogramm abhängig. dh wenn Sie einen Loader der zweiten Klasse haben, der nicht den Loader der ersten Klasse als übergeordnetes Element hat, das dieselbe Aufzählungsklasse lädt, können Sie mehrere Instanzen im Speicher abrufen.


Codebeispiel

Erstellen Sie die folgende Aufzählung, und legen Sie die zugehörige .class-Datei selbst in einem Glas ab. (Natürlich hat das Glas die richtige Paket- / Ordnerstruktur)

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

Führen Sie nun diesen Test aus, und stellen Sie sicher, dass der Klassenpfad keine Kopien der obigen Aufzählung enthält:

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

Und wir haben jetzt zwei Objekte, die den gleichen Enum-Wert darstellen.

Ich bin damit einverstanden, dass dies ein seltener und erfundener Eckfall ist und fast immer eine Aufzählung für einen Java-Singleton verwendet werden kann. Ich mache es selber. Aber die Frage nach möglichen Nachteilen und dieser Hinweis zur Vorsicht ist es wert, darüber informiert zu werden.


Können Sie Hinweise auf dieses Anliegen finden?

Ich habe jetzt meine erste Antwort mit Beispielcode bearbeitet und erweitert. Hoffentlich helfen sie dabei, den Punkt zu veranschaulichen und auch die Frage von MichaelT zu beantworten.
Mad G

@ MichaelT: Ich hoffe, das beantwortet Ihre Frage :-)
Mad G

Wenn der Grund für die Verwendung von enum (anstelle von class) für singleton nur die Sicherheit war, gibt es derzeit keinen Grund dafür ... Excellent, +1
Gangnus

1
Wird sich die "traditionelle" Singleton-Implementierung auch mit zwei verschiedenen Klassenladern erwartungsgemäß verhalten?
Ron Klein

3

Das enum-Muster kann nicht für Klassen verwendet werden, die eine Ausnahme im Konstruktor auslösen würden. Wenn dies erforderlich ist, verwenden Sie eine Fabrik:

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;
    }        
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.