今天我浏览了这个网站上的一些问题,我发现一个枚举被用于单例模式,据称线程安全的好处,这种解决方案。

我从来没用过枚举,而且我已经用Java编程好几年了。显然,他们改变了很多。现在他们甚至在自己内部完全支持OOP。

为什么我应该在日常编程中使用enum ?


当前回答

现在我为什么要用,为什么要用 在日常编程中枚举?

您可以使用Enum来表示较小的固定常数集或内部类模式,同时增加可读性。此外,在方法参数中使用enum时,可以加强一定的刚性。它们提供了一种有趣的可能性,可以将信息传递给一个构造函数,就像在Oracle网站上的Planets示例中那样,而且,正如您所发现的,还允许以一种简单的方式创建一个单例模式。

例:Locale.setDefault(Locale.US)读起来比Locale.setDefault(1)更好,并强制使用IDE中显示的固定值集。分隔符,而不是所有整数。

其他回答

枚举以自记录的方式枚举一组固定的值。 它们使您的代码更显式,也更不容易出错。

为什么不使用String,或int,而不是Enum常量?

编译器不允许错别字,也不允许出值的修正 设置,因为枚举本身就是类型。后果: 你不需要写一个先决条件(或手册)来确保你的论点在有效范围内。 类型不变式是免费的。 枚举可以有行为,就像任何其他类一样。 无论如何,您可能需要类似数量的内存来使用string(这取决于Enum的复杂性)。

此外,Enum的每个实例都是一个类,您可以为其定义单独的行为。

另外,它们在创建实例时(当枚举被加载时)确保线程安全,这在简化单例模式中有很好的应用。

这篇博客说明了它的一些应用,比如解析器的状态机。

使用枚举类型安全,这是一个语言特性,所以你通常会得到:

编译器支持(立即查看类型问题) ide中的工具支持(在切换情况下自动补全,缺失情况,强制默认,…) 在某些情况下,枚举的性能也很好(EnumSet,类型安全的替代传统的基于int的“位标志”)。

枚举可以有方法、构造函数,甚至可以在枚举中使用枚举,并将枚举与接口结合使用。

可以将枚举看作是替换一组定义良好的int常量的类型(Java从C/ c++“继承”了int常量),在某些情况下还可以替换位标志。

《Effective Java 2nd Edition》一书中有一整章是关于它们的,并且有更多的细节。也可以参考Stack Overflow的文章。

为什么要使用编程语言的特性?我们有语言的原因是

程序员以计算机可以使用的形式有效而正确地表达算法。 维护人员理解他人编写的算法并正确地进行更改。

枚举提高了正确性和可读性的可能性,而无需编写大量的样板文件。如果你愿意写样板文件,那么你可以“模拟”枚举:

public class Color {
    private Color() {} // Prevent others from making colors.
    public static final Color RED = new Color();
    public static final Color AMBER = new Color();
    public static final Color GREEN = new Color();
}

现在你可以这样写:

Color trafficLightColor = Color.RED;

上面的样板具有与

public enum Color { RED, AMBER, GREEN };

两者都提供来自编译器的相同级别的检查帮助。样板文件只是更多的输入。但是节省大量的输入使程序员更有效率(见1),所以这是一个值得的特性。

至少还有一个原因是值得的:

Switch语句

上面的静态最终枚举模拟没有给你的一件事是良好的开关情况。对于枚举类型,Java开关使用其变量的类型来推断枚举情况的范围,因此对于上面的枚举Color,你只需要说:

Color color = ... ;
switch (color) {
    case RED:
        ...
        break;
}

注意它不是颜色。箱子里有红色。如果你不使用enum,使用switch的命名量的唯一方法是:

public Class Color {
    public static final int RED = 0;
    public static final int AMBER = 1;
    public static final int GREEN = 2;
}

但是现在保存颜色的变量必须是int类型。漂亮的编译器检查枚举和静态最终模拟消失了。不快乐。

折衷的方法是在模拟中使用标量值成员:

public class Color {
    public static final int RED_TAG = 1;
    public static final int AMBER_TAG = 2;
    public static final int GREEN_TAG = 3;

    public final int tag;

    private Color(int tag) { this.tag = tag; } 
    public static final Color RED = new Color(RED_TAG);
    public static final Color AMBER = new Color(AMBER_TAG);
    public static final Color GREEN = new Color(GREEN_TAG);
}

Now:

Color color = ... ;
switch (color.tag) {
    case Color.RED_TAG:
        ...
        break;
}

但请注意,更多的是样板文件!

使用枚举作为单例

从上面的样板,你可以看到为什么枚举提供了一种实现单例的方法。而不是写:

public class SingletonClass {
    public static final void INSTANCE = new SingletonClass();
    private SingletonClass() {}

    // all the methods and instance data for the class here
}

然后访问它

SingletonClass.INSTANCE

我们可以说

public enum SingletonClass {
    INSTANCE;

    // all the methods and instance data for the class here
}

which gives us the same thing. We can get away with this because Java enums are implemented as full classes with only a little syntactic sugar sprinkled over the top. This is again less boilerplate, but it's non-obvious unless the idiom is familiar to you. I also dislike the fact that you get the various enum functions even though they don't make much sense for the singleton: ord and values, etc. (There's actually a trickier simulation where Color extends Integer that will work with switch, but it's so tricky that it even more clearly shows why enum is a better idea.)

线程安全

只有在没有锁定的情况下惰性地创建单例时,线程安全才会成为潜在的问题。

public class SingletonClass {
    private static SingletonClass INSTANCE;
    private SingletonClass() {}
    public SingletonClass getInstance() {
        if (INSTANCE == null) INSTANCE = new SingletonClass();
        return INSTANCE;
    }

    // all the methods and instance data for the class here
}

如果许多线程同时调用getInstance,而INSTANCE仍然为空,则可以创建任意数量的实例。这很糟糕。唯一的解决方案是添加同步访问来保护变量INSTANCE。

然而,上面的静态最终代码没有这个问题。它在类加载时急切地创建实例。类加载是同步的。

enum单例实际上是惰性的,因为它直到第一次使用才初始化。Java初始化也是同步的,因此多个线程不能初始化instance的多个实例。你得到了一个惰性初始化的单例,只有很少的代码。唯一的缺点是语法相当模糊。您需要了解习惯用法或彻底理解类加载和初始化是如何工作的,才能了解发生了什么。

除了前面提到的用例,我经常发现枚举对于实现策略模式很有用,遵循一些基本的面向对象原则:

将代码放在数据所在的位置(即在枚举本身中——或者通常在枚举常量中,这可能会覆盖方法)。 实现一个(或更多)接口,以便不将客户端代码绑定到枚举(它应该只提供一组默认实现)。

最简单的例子是一组Comparator实现:

enum StringComparator implements Comparator<String> {
    NATURAL {
        @Override
        public int compare(String s1, String s2) {
            return s1.compareTo(s2);
        }
    },
    REVERSE {
        @Override
        public int compare(String s1, String s2) {
            return NATURAL.compare(s2, s1);
        }
    },
    LENGTH {
        @Override
        public int compare(String s1, String s2) {
            return new Integer(s1.length()).compareTo(s2.length());
        }
    };
}

这种“模式”可以在更复杂的场景中使用,广泛使用枚举附带的所有优点:遍历实例,依赖于它们的隐式顺序,根据实例名称检索实例,为特定上下文提供正确实例的静态方法等等。你仍然把这些都隐藏在接口后面,这样你的代码就可以在不需要修改的情况下使用自定义实现,以防你想要一些“默认选项”中不可用的东西。

我曾看到这种方法成功地应用于时间粒度(每天、每周等)概念的建模,其中所有逻辑都封装在枚举中(为给定的时间范围选择正确的粒度,将特定行为绑定到每个粒度作为常量方法等)。而且,服务层所看到的粒度只是一个接口。

除了@BradB,回答:

That is so true... It's strange that it is the only answer who mention that. When beginners discover enums, they quickly take that as a magic-trick for valid identifier checking for the compiler. And when the code is intended to be use on distributed systems, they cry... some month later. Maintain backward compatibility with enums that contains non static list of values is a real concern, and pain. This is because when you add a value to an existing enum, its type change (despite the name does not).

"Ho, wait, it may look like the same type, right? After all, they’re enums with the same name – and aren’t enums just integers under the hood?" And for these reasons, your compiler will likely not flag the use of one definition of the type itself where it was expecting the other. But in fact, they are (in most important ways) different types. Most importantly, they have different data domains – values that are acceptable given the type. By adding a value, we’ve effectively changed the type of the enum and therefore break backward compatibility.

总之:当你想使用它的时候使用它,但是,请检查所使用的数据域是一个有限的、已知的、固定的集合。