Quais são as desvantagens da implementação de um singleton com o enum do 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(){ ... }
}

Tão impressionante quanto a 2ª versão é, há alguma desvantagem nisso?

(Eu dei alguns pensamentos e responderei minha própria pergunta; espero que você tenha respostas melhores)

    
por irreputable 14.12.2012 / 01:39
fonte

3 respostas

31

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 da API. É muito raro que Foo1.getInstance() declare 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.INSTANCE declaramos publicamente que essa instância é a instância e não há chance de alterar isso. A estratégia de implementação de ter uma única instância está exposta e comprometida com.

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

Estendendo a classe Enum

Foo2 estende uma superclasse Enum<Foo2> . Nós geralmente queremos evitar super aulas; especialmente neste caso, a superclasse forçada em Foo2 não tem nada a ver com qual Foo2 deveria ser. Isso é uma poluição para a hierarquia de tipos do nosso aplicativo. Se realmente queremos uma superclasse, geralmente é uma classe de aplicação, mas não podemos, a superclasse de Foo2 é fixa.

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

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() )

Serializabilidade

É muito comum os singletons serem informativos. Esses singletons geralmente devem não ser serializáveis. Não consigo imaginar 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 de forma explícita e precisa o que significa desserializar um singleton em outra VM onde um singleton do mesmo tipo já possa existir.

Foo2 automaticamente se compromete 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 de Foo2 na VM1 em t1, por meio de serialização / desserialização, o valor se tornará um valor diferente - o valor da mesma variável de Foo2 na VM2 em t2, criando uma para detectar bug. Esse bug não acontecerá com o Foo1 silencioso não serializável.

Restrições de codificação

Existem coisas que podem ser feitas em classes normais, mas proibidas em enum classes. Por exemplo, acessando um campo estático no construtor. O programador tem que ser mais cuidadoso já que ele está trabalhando em uma classe especial.

Conclusão

Ao pegar carona no enum, salvamos duas 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 consequências não intencionais. A única alegada vantagem - serialização automática - acaba por ser uma desvantagem.

    
por 14.12.2012 / 02:56
fonte
6

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

Exemplo de código

Crie o seguinte enum e coloque seu arquivo .class em um jar por si só. (claro que o jar terá a estrutura correta de pacote / pasta)

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

Agora, execute este 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 de enum "mesmo".

Concordo que este é um caso de canto raro e artificial, e quase sempre um enum pode ser usado para um singleton de java. Eu mesmo faço isso. Mas a questão sobre potenciais desvantagens e esta nota de cautela vale a pena conhecer.

    
por 09.07.2013 / 18:17
fonte
2

O padrão enum não pode ser usado para nenhuma Classe que possa lançar 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;
    }        
}
    
por 23.04.2014 / 20:02
fonte