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)
Respuestas:
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 deFoo1.getInstance()
puede evolucionar, por ejemplo, para devolver una instancia por hilo.Con
Foo2.INSTANCE
declaramos 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
Foo2
extiende una super claseEnum<Foo2>
. Por lo general, queremos evitar las superclases; especialmente en este caso, la superclase forzadaFoo2
no tiene nada que ver con lo queFoo2
se 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,Foo2
la superclase es fija.Foo2
hereda algunos métodos divertidos de instancia comoname(), cardinal(), compareTo(Foo2)
, que son confusos paraFoo2
los usuarios de.Foo2
no puede tener su propioname()
método incluso si ese método es deseable enFoo2
la interfaz de.Foo2
también contiene algunos métodos estáticos divertidosque 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.
Foo2
se 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 deFoo2
VM1 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 deFoo2
VM2 en t2, creando un error difícil de detectar. Este error no le ocurrirá a los no serializables enFoo1
silencio.Restricciones de codificación
Hay cosas que se pueden hacer en las clases normales, pero prohibidas en las
enum
clases. 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.
fuente
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 ...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)
Ahora ejecute esta prueba, asegurándose de que no haya copias de la enumeración anterior en la ruta de clase:
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.
fuente
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:
fuente