Quels sont les inconvénients de l'implémentation d'un singleton avec l'énumération Java?

14

Traditionnellement, un singleton est généralement implémenté comme

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Avec l'énumération de Java, nous pouvons implémenter un singleton comme

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

Aussi génial que la 2ème version, y a-t-il des inconvénients?

(J'ai réfléchi et je répondrai à ma propre question; j'espère que vous aurez de meilleures réponses)

irréprochable
la source
16
L'inconvénient est qu'il s'agit d'un singleton. Un "schéma" totalement surévalué ( toux )
Thomas Eding

Réponses:

32

Quelques problèmes avec les singletons enum:

S'engager dans une stratégie de mise en œuvre

En général, "singleton" fait référence à une stratégie d'implémentation, et non à une spécification d'API. Il est très rare de Foo1.getInstance()déclarer publiquement qu'il retournera toujours la même instance. Si nécessaire, l'implémentation de Foo1.getInstance()peut évoluer, par exemple, pour renvoyer une instance par thread.

Avec Foo2.INSTANCEnous déclarons publiquement que cette instance est l' instance, et il n'y a aucune chance de changer cela. La stratégie de mise en œuvre consistant à avoir une seule instance est exposée et engagée.

Ce problème n'est pas paralysant. Par exemple, Foo2.INSTANCE.doo()peut s'appuyer sur un objet d'assistance local de thread, pour avoir effectivement une instance par thread.

Extension de la classe Enum

Foo2étend une super classe Enum<Foo2>. Nous voulons généralement éviter les super classes; surtout dans ce cas, la super classe forcée Foo2n'a rien à voir avec ce qui Foo2est censé être. C'est une pollution de la hiérarchie des types de notre application. Si nous voulons vraiment une super classe, c'est généralement une classe d'application, mais nous ne pouvons pas, Foo2la super classe de est fixe.

Foo2hérite de quelques méthodes d'instance amusantes comme name(), cardinal(), compareTo(Foo2), qui sont juste déroutantes pour Foo2les utilisateurs de. Foo2ne peut pas avoir sa propre name()méthode même si cette méthode est souhaitable dans Foo2l'interface de.

Foo2 contient également des méthodes statiques drôles

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

qui semble être absurde pour les utilisateurs. Un singleton ne devrait généralement pas avoir de méthodes statiques pulbiques (autres que les getInstance())

Sérialisabilité

Il est très courant que les singletons soient avec état. Ces singletons ne doivent généralement pas être sérialisables. Je ne peux penser à aucun exemple réaliste où il est logique de transporter un singleton avec état d'une VM à une autre VM; un singleton signifie «unique au sein d'une machine virtuelle», et non «unique dans l'univers».

Si la sérialisation a vraiment un sens pour un singleton avec état, le singleton doit spécifier explicitement et précisément ce que signifie désérialiser un singleton dans une autre VM où un singleton du même type peut déjà exister.

Foo2s'engage automatiquement dans une stratégie simpliste de sérialisation / désérialisation. Ce n'est qu'un accident qui attend de se produire. Si nous avons un arbre de données référençant conceptuellement une variable d'état de Foo2dans VM1 à t1, par la sérialisation / désérialisation, la valeur devient une valeur différente - la valeur de la même variable de Foo2dans VM2 à t2, créant un bug difficile à détecter. Ce bug n'arrivera pas à l'insérialisable en Foo1silence.

Restrictions de codage

Il y a des choses qui peuvent être faites dans les classes normales, mais interdites dans les enumclasses. Par exemple, accéder à un champ statique dans le constructeur. Le programmeur doit être plus prudent car il travaille dans une classe spéciale.

Conclusion

En superposant sur enum, nous économisons 2 lignes de code; mais le prix est trop élevé, nous devons transporter tous les bagages et restrictions des énumérations, nous héritons par inadvertance des "caractéristiques" de l'énumération qui ont des conséquences inattendues. Le seul avantage allégué - la sérialisation automatique - se révèle être un inconvénient.

irréprochable
la source
2
-1: Votre discussion sur la sérialisation est fausse. Le mécanisme n'est pas simpliste car les énumérations sont traitées très différemment des instances régulières pendant la désérialisation. Le problème décrit ne se produit pas car le mécanisme de désérialisation réel ne modifie pas une "variable d'état".
scarfridge
voir cet exemple de confusion: coderanch.com/t/498782/java/java/…
irréputable
2
En fait, la discussion liée souligne mon point. Permettez-moi d'expliquer ce que j'ai compris comme étant le problème que vous prétendez exister. Certains objets A font référence à un deuxième objet B. L'instance singleton S fait également référence à B. Désérialisons maintenant une instance précédemment sérialisée du singleton basé sur une énumération (qui, au moment de la sérialisation, faisait référence à B '! = B). Ce qui se passe réellement, c'est que A et S référencent B, car B 'ne sera pas sérialisé. Je pensais que tu voulais exprimer que A et S ne font plus référence au même objet.
scarfridge
1
Peut-être que nous ne parlons pas vraiment du même problème?
scarfridge
1
@Kevin Krumwiede:, Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);par exemple un très bel exemple serait :; Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);^), testé sous Java 7 et Java 8…
Holger
6

Une instance d'énumération dépend du chargeur de classe. c'est-à-dire que si vous avez un chargeur de deuxième classe qui n'a pas le chargeur de première classe en tant que parent chargeant la même classe d'énumération, vous pouvez obtenir plusieurs instances en mémoire.


Exemple de code

Créez l'énumération suivante et placez son fichier .class dans un bocal par lui-même. (bien sûr, le pot aura la bonne structure de package / dossier)

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

Exécutez maintenant ce test, en vous assurant qu'il n'y a pas de copie de l'énumération ci-dessus sur le chemin de 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);
}

Et nous avons maintenant deux objets représentant la "même" valeur d'énumération.

Je suis d'accord qu'il s'agit d'un cas d'angle rare et artificiel, et presque toujours une énumération peut être utilisée pour un singleton java. Je le fais moi-même. Mais la question posée sur les inconvénients potentiels et cette note de prudence mérite d'être connue.

Mad G
la source
Pouvez-vous trouver des références à cette préoccupation?
J'ai maintenant modifié et amélioré ma réponse initiale avec un exemple de code. J'espère qu'ils aideront à illustrer le point et à répondre également à la requête de MichaelT.
Mad G
@MichaelT: J'espère que cela répond à votre question :-)
Mad G
Donc, si la raison de l'utilisation d'énumération (au lieu de classe) pour singleton n'était que sa sécurité, il n'y a plus aucune raison pour cela maintenant ... Excellent, +1
Gangnus
1
L'implémentation singleton "traditionnelle" se comportera-t-elle comme prévu, même avec deux chargeurs de classe différents?
Ron Klein
3

Le modèle enum ne peut être utilisé pour aucune classe qui lèverait une exception dans le constructeur. Si cela est nécessaire, utilisez une usine:

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;
    }        
}
user3158036
la source