用Java枚举实现单例的缺点是什么?


14

传统上,单例通常实现为

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

    public static Foo1 getInstance(){ return INSTANCE; }

    private Foo1(){}

    public void doo(){ ... }
}

使用Java的枚举,我们可以实现单例为

public enum Foo2
{
    INSTANCE;

    public void doo(){ ... }
}

与第二版一样出色,它有什么缺点吗?

(我考虑了一下,我会回答我自己的问题;希望您有更好的答案)


16
缺点是它是一个单例。完全被高估的(咳嗽)“模式”
Thomas Eding 2012年

Answers:


32

枚举单例的一些问题:

致力于实施策略

通常,“单一”是指一种实施策略,而不是API规范。Foo1.getInstance()公开声明它将始终返回同一实例的情况很少见。如果需要,例如的实现Foo1.getInstance()可以发展为每个线程返回一个实例。

随着Foo2.INSTANCE我们公开宣布这个实例实例,而且也没有机会来改变这种状况。具有单个实例的实现策略已公开并落实。

这个问题并不严重。例如,Foo2.INSTANCE.doo()可以依赖线程本地帮助器对象,以有效地具有每个线程的实例。

扩展枚举类

Foo2扩展了一个超类Enum<Foo2>。我们通常想避免超类;特别是在这种情况下,被强加的超类Foo2Foo2假定的情况无关。这对我们应用程序的类型层次结构造成了污染。如果我们真的想要一个超类,通常它是一个应用程序类,但是我们不能,它Foo2的超类是固定的。

Foo2继承了一些有趣的实例方法(例如)name(), cardinal(), compareTo(Foo2),这些方法只会使Foo2用户感到困惑。即使在接口中需要该方法Foo2也不能拥有自己的name()方法Foo2

Foo2 还包含一些有趣的静态方法

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

这对用户而言似乎毫无意义。无论如何,单身人士通常都不应使用脉冲静态方法(除外getInstance()

可序列化

单身人士有状态是很常见的。这些单例通常应该可序列化。我想不出任何现实的例子,将有状态的单例从一个VM传输到另一个VM是有意义的。单例表示“在VM中是唯一的”,而不是“在Universe中是唯一的”。

如果序列化对于有状态单例确实有意义,则该单例应明确且精确地指定在可能已存在相同类型单例的另一个VM中反序列化单例的含义。

Foo2自动采用简单的序列化/反序列化策略。那只是一场等待发生的事故。如果我们有一个数据树,从概念上讲Foo2在t1时引用了VM1中的状态变量,则通过序列化/反序列化,该值将变为不同的值- Foo2在t2时VM2 中的相同变量的值,从而导致难以检测的bug。该错误不会Foo1静默地出现在不可序列化的代码中。

编码限制

有一些事情可以在普通课堂上完成,但在enum课堂上是被禁止的。例如,访问构造函数中的静态字段。程序员必须在特殊的班级工作,因此必须更加小心。

结论

通过搭载枚举,我们节省了两行代码;但是价格太高,我们必须背负枚举的所有包and和限制,我们无意间继承了枚举的“功能”,这些功能会带来意想不到的后果。唯一据称的优势-自动可序列化性-成为劣势。


2
-1:您对序列化的讨论是错误的。该机制并不简单,因为在反序列化过程中枚举与常规实例的处理方式非常不同。由于实际的反序列化机制不会修改“状态变量” ,因此不会发生所描述的问题。
围巾岭12/12/14


2
实际上,相关讨论强调了我的观点。让我解释一下我理解为您声称存在的问题。一些对象A引用了第二个对象B。单例实例S也引用了B。现在,我们反序列化基于枚举的单例的先前序列化的实例(在序列化时,引用为B'!= B)。实际上发生的是,A S引用了B,因为B'不会被序列化。我以为您想表达A和S不再引用相同的对象。
围巾岭12/12/14

1
也许我们不是真的在谈论同一问题?
围巾岭

1
@Kevin Krumwiede:Constructor<?> c=EnumType.class.getDeclaredConstructors()[0]; c.setAccessible(true); EnumType f=(EnumType)MethodHandles.lookup().unreflectConstructor(c).invokeExact("BAR", 1);例如,一个非常好的示例是:Constructor<?> c=Thread.State.class.getDeclaredConstructors()[0]; c.setAccessible(true); Thread.State f=(Thread.State)MethodHandles.lookup().unreflectConstructor(c).invokeExact("RUNNING_BACKWARD", -1);; ^),已在Java 7和Java 8下进行了测试…
Holger 2015年

6

枚举实例取决于类加载器。即,如果您有第二个类加载器,而没有第一个类加载器作为父级加载相同的枚举类,则可以在内存中获取多个实例。


代码样例

创建以下枚举,然后将其.class文件单独放入jar中。(当然,罐子将具有正确的包装/文件夹结构)

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

现在运行此测试,确保在类路径上没有上述枚举的副本:

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

现在,我们有两个对象代表“相同”的枚举值。

我同意这是一种罕见且人为的极端情况,几乎所有枚举都可用于Java单例。我自己做。但是这个问题被问到潜在的负面影响,这一注意事项值得了解。


您能找到有关该问题的任何参考资料吗?

我现在用示例代码编辑和增强了我的初始答案。希望他们能帮助说明这一点并回答MichaelT的疑问。
Mad G

@MichaelT:我希望能回答您的问题:-)
Mad G

因此,如果仅出于安全性考虑使用枚举(而不是类)是因为它的安全性,那么现在就没有任何理由...优秀,+ 1
Gangnus 2015年

1
即使使用两个不同的类加载器,“传统”单例实现是否也会按预期运行?
罗恩·克莱因

3

枚举模式不能用于任何会在构造函数中引发异常的类。如果需要,请使用工厂:

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;
    }        
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.