Quais são as desvantagens de implementar um singleton com a enumeração de Java?


14

Tradicionalmente, um singleton é geralmente implementado como

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

Com o enum do Java, podemos implementar um singleton como

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

Por mais incrível que seja a 2ª versão, existem algumas desvantagens?

(Pensei um pouco e responderei minha própria pergunta; espero que você tenha respostas melhores)


16
A desvantagem é que é um singleton. Um "padrão" totalmente superestimado ( tosse )
Thomas Eding

Respostas:


32

Alguns problemas com enum singletons:

Comprometendo-se com uma estratégia de implementação

Normalmente, "singleton" refere-se a uma estratégia de implementação, não a uma especificação de API. É muito raro Foo1.getInstance()declarar publicamente que sempre retornará a mesma instância. Se necessário, a implementação de Foo1.getInstance()pode evoluir, por exemplo, para retornar uma instância por encadeamento.

Com Foo2.INSTANCEdeclaramos publicamente que neste caso é o exemplo, e não há nenhuma chance de mudar isso. A estratégia de implementação de ter uma única instância é exposta e comprometida.

Este problema não é incapacitante. Por exemplo, Foo2.INSTANCE.doo()pode contar com um objeto auxiliar local de encadeamento, para ter efetivamente uma instância por encadeamento.

Estendendo a classe Enum

Foo2estende uma super classe Enum<Foo2>. Geralmente, queremos evitar super classes; especialmente neste caso, a superclasse forçada Foo2não tem nada a ver com o que Foo2deveria ser. Isso é uma poluição para a hierarquia de tipos de nosso aplicativo. Se realmente queremos uma super classe, geralmente é uma classe de aplicação, mas não podemos, Foo2a super classe é fixa.

Foo2herda alguns métodos engraçados de instância como name(), cardinal(), compareTo(Foo2), que são apenas confusos para Foo2os usuários. Foo2não pode ter seu próprio name()método, mesmo que esse método seja desejável na Foo2interface.

Foo2 também contém alguns métodos estáticos engraçados

    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 ser sem sentido para os usuários. Um singleton geralmente não deve ter métodos estáticos pulbicos de qualquer maneira (além do getInstance())

Serializability

É muito comum os singletons serem stateful. Esses singletons geralmente não devem ser serializáveis. Não consigo pensar em nenhum exemplo realista em que faça sentido transportar um singleton com estado de uma VM para outra VM; um singleton significa "único dentro de uma VM", não "único no universo".

Se a serialização realmente faz sentido para um singleton com estado, o singleton deve especificar explícita e precisamente o que significa desserializar um singleton em outra VM em que um singleton do mesmo tipo já exista.

Foo2se compromete automaticamente com uma estratégia simplista de serialização / desserialização. Isso é apenas um acidente esperando para acontecer. Se tivermos uma árvore de dados referenciando conceitualmente uma variável de estado da Foo2VM1 em t1, por serialização / desserialização, o valor se tornará um valor diferente - o valor da mesma variável da Foo2VM2 em t2, criando um erro difícil de detectar. Este bug não acontecerá com o unserializable Foo1silenciosamente.

Restrições de codificação

Há coisas que podem ser feitas nas aulas normais, mas proibidas nas enumaulas. Por exemplo, acessando um campo estático no construtor. O programador precisa ter mais cuidado, pois está trabalhando em uma aula especial.

Conclusão

Ao pegar carona no enum, salvamos 2 linhas de código; mas o preço é muito alto, temos que carregar todas as bagagens e restrições de enums, inadvertidamente herdamos "características" de enum que têm conseqüências não intencionais. A única vantagem alegada - serialização automática - acaba sendo uma desvantagem.


2
-1: sua discussão sobre serialização está errada. O mecanismo não é simplista, pois as enumerações são tratadas de maneira muito diferente das instâncias regulares durante a desserialização. O problema descrito não ocorre porque o mecanismo de desserialização real não modifica uma "variável de estado".
scarfridge

veja este exemplo de confusão: coderanch.com/t/498782/java/java/…
irreputable

2
Na verdade, a discussão vinculada enfatiza meu argumento. Deixe-me explicar o que entendi como o problema que você afirma existir. Algum objeto A faz referência a um segundo objeto B. A instância singleton S também faz referência a B. Agora, desserializamos uma instância serializada anteriormente do singleton baseado em enum (que, no momento da serialização, referenciava B '! = B). O que realmente acontece é que as referências A e S B, porque B 'não serão serializadas. Eu pensei que você queria expressar que A e S não fazem mais referência ao mesmo objeto.
scarfridge

1
Talvez não estejamos realmente falando sobre o mesmo 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 exemplo, um exemplo muito bom seria :; Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);^), testado em Java 7 e Java 8…
Holger

6

Uma instância de enum depende do carregador de classes. ou seja, se você tiver um carregador de segunda classe que não tenha o carregador de primeira classe como pai carregando a mesma classe enum, poderá obter várias instâncias na memória.


Amostra de código

Crie a enumeração a seguir e coloque o arquivo .class em um jar sozinho. (é claro que o jar terá a estrutura correta de pacote / pasta)

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

Agora execute esse teste, certificando-se de que não haja cópias da enumeração acima no caminho da 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 agora temos dois objetos representando o valor "enum" mesmo.

Concordo que este é um caso de canto raro e artificial, e quase sempre um enum pode ser usado para um singleton em java. Eu mesmo faço isso. Mas a pergunta feita sobre possíveis desvantagens e vale a pena conhecer essa nota de cautela.


Você pode encontrar alguma referência a essa preocupação?

Agora editei e aprimorei minha resposta inicial com código de exemplo. Espero que eles também ajudem a ilustrar o ponto e respondam à pergunta de MichaelT.
Mad G

@MichaelT: Espero que responde a sua pergunta :-)
Mad G

Assim, se a razão para o uso de enumeração (em vez de classe) para Singleton era apenas a sua segurança, não há qualquer razão para isso agora ... Excelente, +1
Gangnus

1
A implementação singleton "tradicional" se comportará conforme o esperado, mesmo com dois carregadores de classes diferentes?
Ron Klein

3

O padrão enum não pode ser usado para nenhuma classe que lança uma exceção no construtor. Se isso for necessário, use uma 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;
    }        
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.