Quali sono gli svantaggi dell'implementazione di un singleton con l'enum di Java?


14

Tradizionalmente, un singleton è di solito implementato come

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Con l'enum di Java, possiamo implementare un singleton come

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

Fantastico come la seconda versione, ci sono degli svantaggi?

(Ci ho pensato e risponderò alla mia domanda; spero che tu abbia risposte migliori)


16
Il rovescio della medaglia è che è un singleton. Uno "schema" totalmente sopravvalutato ( tosse )
Thomas Eding,

Risposte:


32

Alcuni problemi con i singoli enum:

Impegnarsi in una strategia di attuazione

In genere, "singleton" si riferisce a una strategia di implementazione, non a una specifica API. È molto raro Foo1.getInstance()dichiarare pubblicamente che restituirà sempre la stessa istanza. Se necessario, l'implementazione di Foo1.getInstance()può evolversi, ad esempio, per restituire un'istanza per thread.

Con Foo2.INSTANCEdichiariamo pubblicamente che questa istanza è l' istanza e non c'è alcuna possibilità di cambiarla. La strategia di implementazione di avere una singola istanza è esposta e impegnata.

Questo problema non è paralizzante. Ad esempio, Foo2.INSTANCE.doo()può fare affidamento su un oggetto helper locale di thread per disporre effettivamente di un'istanza per thread.

Estensione della classe Enum

Foo2estende una super classe Enum<Foo2>. Di solito vogliamo evitare le super classi; specialmente in questo caso, la superclasse forzata Foo2non ha nulla a che fare con ciò che Foo2dovrebbe essere. Questo è un inquinamento per la gerarchia dei tipi della nostra applicazione. Se vogliamo davvero una super classe, di solito è una classe di applicazione, ma non possiamo,Foo2 la superclasse è fissa.

Foo2eredita alcuni metodi di istanza divertenti come name(), cardinal(), compareTo(Foo2), che sono solo fonte di confusione per Foo2gli utenti. Foo2non può avere il proprio name()metodo anche se tale metodo è desiderabile Foo2nell'interfaccia di.

Foo2 contiene anche alcuni metodi statici divertenti

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

che sembra essere privo di senso per gli utenti. Un singleton di solito non dovrebbe avere comunque metodi statici pulbici (diversi dai getInstance())

serializzabilità

È molto comune che i singoli siano con stato. Questi singleton generalmente non dovrebbero essere serializzabili. Non riesco a pensare a nessun esempio realistico in cui abbia senso trasportare un singleton con stato da una VM a un'altra VM; un singleton significa "unico all'interno di una VM", non "unico nell'universo".

Se la serializzazione ha davvero senso per un singleton con stato, il singleton dovrebbe specificare in modo esplicito e preciso cosa significa deserializzare un singleton in un'altra VM in cui potrebbe già esistere un singleton dello stesso tipo.

Foo2si impegna automaticamente in una strategia di serializzazione / deserializzazione semplicistica. Questo è solo un incidente in attesa di accadere. Se abbiamo un albero di dati che fa riferimento concettualmente a una variabile di stato Foo2in VM1 at t1, attraverso la serializzazione / deserializzazione il valore diventa un valore diverso - il valore della stessa variabile di Foo2in VM2 at t2, creando un bug difficile da rilevare. Questo bug non accadrà Foo1silenziosamente all'inserializzabile .

Restrizioni alla codifica

Ci sono cose che possono essere fatte in classi normali, ma vietate in enumclasse. Ad esempio, l'accesso a un campo statico nel costruttore. Il programmatore deve stare più attento poiché lavora in una classe speciale.

Conclusione

Salvando su enum, salviamo 2 righe di codice; ma il prezzo è troppo alto, dobbiamo portare tutti i bagagli e le restrizioni degli enum, ereditiamo inavvertitamente "caratteristiche" dell'enum che hanno conseguenze indesiderate. L'unico presunto vantaggio - serializzabilità automatica - risulta essere uno svantaggio.


2
-1: la tua discussione sulla serializzazione è sbagliata. Il meccanismo non è semplicistico in quanto gli enum vengono trattati in modo molto diverso dai casi normali durante la deserializzazione. Il problema descritto non si verifica poiché l'attuale meccanismo di deserializzazione non modifica una "variabile di stato".
scarfridge,

vedi questo esempio di confusione: coderanch.com/t/498782/java/java/…
irreputabile

2
In realtà la discussione collegata sottolinea il mio punto. Lasciami spiegare quello che ho capito come il problema che pretendi di esistere. Alcuni oggetti A fanno riferimento a un secondo oggetto B. Anche l'istanza singleton S fa riferimento a B. Ora deserializziamo un'istanza precedentemente serializzata del singleton basato su enum (che, al momento della serializzazione, faceva riferimento a B '! = B). Ciò che realmente accade è che A e S fanno riferimento a B, perché B 'non verrà serializzato. Pensavo volessi esprimere che A e S non fanno più riferimento allo stesso oggetto.
scarfridge,

1
Forse non stiamo davvero parlando dello stesso problema?
scarfridge il

1
@Kevin Krumwiede: Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);ad esempio un esempio davvero interessante sarebbe :; Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);^), testato con Java 7 e Java 8 ...
Holger,

6

Un'istanza enum dipende dal caricatore di classi. cioè se hai un caricatore di seconda classe che non ha il caricatore di prima classe come genitore che carica la stessa classe enum puoi ottenere più istanze in memoria.


Esempio di codice

Crea il seguente enum e metti il ​​suo file .class in un barattolo da solo. (ovviamente il vaso avrà la struttura del pacchetto / cartella corretta)

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

Ora esegui questo test, assicurandoti che non ci siano copie dell'enum sopra nel percorso della classe:

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

E ora abbiamo due oggetti che rappresentano lo "stesso" valore enum.

Sono d'accordo che questo è un caso angolare raro e inventato, e quasi sempre un enum può essere usato per un singleton java. Lo faccio da solo. Ma la domanda posta sui potenziali aspetti negativi e questa nota di cautela merita di essere conosciuta.


Riesci a trovare riferimenti a tale preoccupazione?

Ora ho modificato e migliorato la mia risposta iniziale con un codice di esempio. Speriamo che possano aiutare a illustrare il punto e rispondere anche alla domanda di MichaelT.
Mad G

@MichaelT: Spero che risponda alla tua domanda :-)
Mad G

Quindi, se il motivo dell'uso di enum (anziché della classe) per singleton era solo la sua sicurezza, non c'è motivo per farlo ora ... Eccellente, +1
Gangnus,

1
L'implementazione singleton "tradizionale" si comporterà come previsto anche con due caricatori di classi differenti?
Ron Klein,

3

il modello enum non può essere usato per nessuna Classe che genererebbe un'eccezione nel costruttore. Se necessario, utilizzare una fabbrica:

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;
    }        
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.