¿Cuáles son las desventajas de implementar un singleton con la enumeración de Java?

14

Tradicionalmente, un singleton generalmente se implementa como

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Con la enumeración de Java, podemos implementar un singleton como

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

Tan increíble como es la segunda versión, ¿hay alguna desventaja?

(Lo pensé y responderé mi propia pregunta; espero que tengas mejores respuestas)

irreputable
fuente
16
La desventaja es que es un singleton. Un "patrón" totalmente sobrevalorado ( tos )
Thomas Eding

Respuestas:

32

Algunos problemas con enum singletons:

Comprometerse con una estrategia de implementación

Normalmente, "singleton" se refiere a una estrategia de implementación, no a una especificación de API. Es muy raro Foo1.getInstance()declarar públicamente que siempre devolverá la misma instancia. Si es necesario, la implementación de Foo1.getInstance()puede evolucionar, por ejemplo, para devolver una instancia por hilo.

Con Foo2.INSTANCEdeclaramos públicamente que esta instancia es la instancia, y no hay posibilidad de cambiar eso. La estrategia de implementación de tener una sola instancia está expuesta y comprometida.

Este problema no es paralizante. Por ejemplo, Foo2.INSTANCE.doo()puede confiar en un objeto auxiliar local de subprocesos para tener una instancia por subproceso de manera efectiva .

Ampliando la clase Enum

Foo2extiende una super clase Enum<Foo2>. Por lo general, queremos evitar las superclases; especialmente en este caso, la superclase forzada Foo2no tiene nada que ver con lo que Foo2se supone que es. Eso es una contaminación para la jerarquía de tipos de nuestra aplicación. Si realmente queremos una superclase, generalmente es una clase de aplicación, pero no podemos, Foo2la superclase es fija.

Foo2hereda algunos métodos divertidos de instancia como name(), cardinal(), compareTo(Foo2), que son confusos para Foo2los usuarios de. Foo2no puede tener su propio name()método incluso si ese método es deseable en Foo2la interfaz de.

Foo2 también contiene algunos métodos estáticos divertidos

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

que parece no tener sentido para los usuarios. Un singleton generalmente no debería tener métodos estáticos pulbicos de todos modos (aparte del getInstance())

Serializabilidad

Es muy común que los singletons tengan estado. Estos singletons generalmente no deben ser serializables. No se me ocurre ningún ejemplo realista en el que tenga sentido transportar un singleton con estado de una VM a otra VM; un singleton significa "único dentro de una VM", no "único en el universo".

Si la serialización realmente tiene sentido para un singleton con estado, el singleton debería especificar de manera explícita y precisa qué significa deserializar un singleton en otra VM donde ya puede existir un singleton del mismo tipo.

Foo2se compromete automáticamente con una estrategia de serialización / deserialización simplista. Eso es solo un accidente esperando a suceder. Si tenemos un árbol de datos que hace referencia conceptual a una variable de estado de Foo2VM1 en t1, a través de la serialización / deserialización, el valor se convierte en un valor diferente: el valor de la misma variable de Foo2VM2 en t2, creando un error difícil de detectar. Este error no le ocurrirá a los no serializables en Foo1silencio.

Restricciones de codificación

Hay cosas que se pueden hacer en las clases normales, pero prohibidas en las enumclases. Por ejemplo, acceder a un campo estático en el constructor. El programador tiene que tener más cuidado ya que está trabajando en una clase especial.

Conclusión

Al llevar a cuestas enum, ahorramos 2 líneas de código; pero el precio es demasiado alto, tenemos que llevar todos los equipajes y restricciones de las enumeraciones, heredamos inadvertidamente "características" de la enumeración que tienen consecuencias no deseadas. La única supuesta ventaja (serialización automática) resulta ser una desventaja.

irreputable
fuente
2
-1: Su discusión sobre la serialización es incorrecta. El mecanismo no es simplista ya que las enumeraciones se tratan de manera muy diferente a las instancias regulares durante la deserialización. El problema descrito no ocurre ya que el mecanismo de deserialización real no modifica una "variable de estado".
scarfridge
vea este ejemplo de confusión: coderanch.com/t/498782/java/java/…
irreputable el
2
En realidad, la discusión vinculada enfatiza mi punto. Déjame explicarte lo que entendí como el problema que afirmas que existe. Algún objeto A hace referencia a un segundo objeto B. La instancia única S hace referencia a B también. Ahora deserializamos una instancia previamente serializada del singleton basado en enumeración (que, en el momento de la serialización, hacía referencia a B '! = B). Lo que realmente sucede es que A y S hacen referencia a B, porque B 'no se serializará. Pensé que querías expresar que A y S ya no hacen referencia al mismo objeto.
scarfridge
1
¿Quizás no estamos hablando realmente del mismo problema?
scarfridge
1
@Kevin Krumwiede: Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);por ejemplo, un buen ejemplo sería :; Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);^), probado en Java 7 y Java 8 ...
Holger
6

Una instancia de enum depende del cargador de clases. es decir, si tiene un cargador de segunda clase que no tiene el cargador de primera clase como padre que carga la misma clase de enumeración, puede obtener varias instancias en la memoria.


Muestra de código

Cree la siguiente enumeración y coloque su archivo .class en un jar por sí mismo. (por supuesto, el jar tendrá la estructura correcta de paquete / carpeta)

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

Ahora ejecute esta prueba, asegurándose de que no haya copias de la enumeración anterior en la ruta de clase:

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

Y ahora tenemos dos objetos que representan el "mismo" valor de enumeración.

Estoy de acuerdo en que este es un caso de esquina raro y artificial, y casi siempre se puede usar una enumeración para un singleton de Java. Lo hare yo mismo. Pero vale la pena conocer la pregunta sobre posibles desventajas y esta nota de precaución.

Mad G
fuente
¿Puedes encontrar alguna referencia a esa preocupación?
Ahora he editado y mejorado mi respuesta inicial con código de ejemplo. Con suerte, ayudarán a ilustrar el punto y responderán la consulta de MichaelT también.
Mad G
@MichaelT: Espero que eso responda tu pregunta :-)
Mad G
Entonces, si la razón para usar enum (en lugar de clase) para singleton era solo su seguridad, no hay ninguna razón para eso ahora ... Excelente, +1
Gangnus
1
¿La implementación singleton "tradicional" se comportará como se espera incluso con dos cargadores de clases diferentes?
Ron Klein
3

el patrón enum no se puede usar para ninguna clase que arroje una excepción en el constructor. Si esto es necesario, use una fábrica:

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;
    }        
}
usuario3158036
fuente