什么是枚举,它们为什么有用?

601
今天我在这个网站上浏览了一些问题,我发现提到了关于单例模式中使用枚举的用法,声称这种解决方案具有线程安全性优势。
我从未使用过枚举,在Java编程中已经有几年了。显然,它们已经发生了很大变化。现在它们甚至可以在自身内部进行完整的面向对象编程支持。
那么,我在日常编程中为什么要使用枚举?

10
在他的书《Effective Java, Second Edition》中,Joshua Bloch在“Item 3: Enforce the Singleton Property with a Private Constructor or an enum Type”一章中详细阐述了这种方法。该章节已经在Dr. Dobb's上重新印刷。 - trashgod
1
你不能实例化枚举,因为它有私有构造函数,它们在 JVM 启动时被实例化,所以是单例的。枚举不是线程安全的。 - Arnav Joshi
29个回答

771

当一个变量(尤其是方法参数)只能从一小组可能值中选择时,应始终使用枚举。例如,类型常量(合同状态:“permanent”、“temp”、“apprentice”)或标志(“立即执行”,“延迟执行”等)。

如果您使用枚举而不是整数(或字符串代码),则可以增加编译时检查并避免由于传递无效常量而导致的错误,并记录哪些值是合法的。

顺便提一下,过度使用枚举可能意味着您的方法功能过于复杂(通常最好拥有几个单独的方法,而不是一个方法接受几个标志来修改其操作方式),但如果必须使用标志或类型代码,则应使用枚举。

例如,哪个更好?

/** Counts number of foobangs.
 * @param type Type of foobangs to count. Can be 1=green foobangs,
 * 2=wrinkled foobangs, 3=sweet foobangs, 0=all types.
 * @return number of foobangs of type
 */
public int countFoobangs(int type)

对比

/** Types of foobangs. */
public enum FB_TYPE {
 GREEN, WRINKLED, SWEET, 
 /** special type for all types combined */
 ALL;
}

/** Counts number of foobangs.
 * @param type Type of foobangs to count
 * @return number of foobangs of type
 */
public int countFoobangs(FB_TYPE type)

像这样的方法调用:

int sweetFoobangCount = countFoobangs(3);

然后变成:

int sweetFoobangCount = countFoobangs(FB_TYPE.SWEET);
在第二个示例中,可以立即了解允许使用哪些类型,文档和实现不能不同步,编译器可以强制执行此操作。同时,像下面这样的无效调用:
int sweetFoobangCount = countFoobangs(99);

不再可能。


52
如果你使用枚举并希望允许数值的组合,可以使用EnumSet。这个类提供了各种实用的辅助方法,例如公共静态常量EnumSet<FB_TYPE> ALL = EnumSet.allOf(FB_TYPE.class)。 - Rob Audenaerde
34
我真的很喜欢在SO上看到这些答案,因为有人付出了真正的努力,干得好!现在我理解了枚举! - Dediqated
1
我不认为“当一个变量只能取一小组可能值中的一个时,你应该总是使用枚举”这种说法是正确的。在实时应用程序中,由于内存短缺,将常量值用作“二进制标志”(通过逻辑或运算)可能非常有用。 - Elist
3
使用枚举可以提高代码可读性,如果在某些时候使用常量,你或者你的接班人可能不知道为什么要使用它... :) - theRoot
著名的例子:枚举花色 { 方块, 红桃, 梅花, 黑桃 } - MC Emperor

154
为什么使用任何编程语言特性?我们拥有语言的原因是为了:
1. 程序员能够高效且正确地表达算法,以便计算机使用。 2. 维护者能够理解其他人编写的算法并正确地进行更改。
枚举可以提高正确性和可读性,而不需要编写大量样板代码。如果您愿意编写样板代码,那么您可以“模拟”枚举:
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 语句 上面的 static final 枚举模拟并没有为您提供漂亮的 switch case。对于枚举类型,Java switch 使用其变量类型来推断枚举 case 的范围,因此对于上面的 enum Color,您只需要说:
Color color = ... ;
switch (color) {
    case RED:
        ...
        break;
}

注意,在这些情况下不是使用Color.RED。如果您不使用枚举,那么使用具有switch的命名数量的唯一方法类似于:
public Class Color {
    public static final int RED = 0;
    public static final int AMBER = 1;
    public static final int GREEN = 2;
}

现在必须将用于保存颜色的变量的类型设置为int 。枚举和static final 的编译器检查已经消失了,这让我们很不开心。
一个妥协方法是在模拟中使用标量值成员。
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);
}

现在:
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
}

这给我们带来了同样的东西。我们可以这样做是因为Java枚举类型被实现为仅有一点点语法糖覆盖的完整类。这减少了样板代码,但除非你熟悉这种惯用语,否则它并不明显。我也不喜欢即使对于单例来说它们并没有太多意义,你还是会得到各种枚举函数:ord和values等(实际上有一个更棘手的模拟,其中Color扩展Integer,可以与switch一起使用,但它如此棘手,以至于更清楚地表明为什么枚举是一个更好的想法)。

线程安全

只有在单例使用惰性创建且没有锁时,线程安全才是一个潜在的问题。

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
}

如果许多线程在INSTANCE仍为空的情况下同时调用getInstance,则可能创建任意数量的实例。 这很糟糕。 唯一的解决方案是添加synchronized访问以保护变量INSTANCE
然而,上面的static final代码没有这个问题。 它在类加载时急切地创建实例。 类加载是同步的。 enum单例实际上是懒惰的,因为它直到第一次使用才会初始化。 Java初始化也是同步的,因此多个线程不能初始化多个INSTANCE实例。 您将获得一个带有非常少代码的延迟初始化的单例。 唯一的负面影响是语法相当晦涩。 您需要知道这种习惯用法或深入了解类加载和初始化的工作原理才能知道发生了什么。

19
那最后一段真的为我澄清了单例模式的情况。谢谢!任何匆匆浏览的读者都应该重新阅读那一段。 - Basil Bourque
我正在阅读这篇文章,https://dev59.com/tmQn5IYBdhLWcg3wg3aR 。现在我对你的最后一段话感到困惑。枚举如何不提供延迟初始化功能? - Deepankar Singh
static final 字段在类初始化时被初始化,而不是加载时。这与 enum 常量初始化完全相同(实际上,在底层它们是相同的)。这就是为什么即使在最初的 Java 版本中,尝试实现单例模式的“聪明”延迟初始化代码总是无意义的原因。 - Holger

49
除了已经提到的用例,我经常发现枚举在实现策略模式时非常有用,遵循一些基本的面向对象编程指南:
1. 在数据所在的代码中(也就是在枚举本身内部或常量内部,常量可以覆盖方法)。 2. 实现一个或多个接口,以便不将客户端代码绑定到枚举上(枚举应该只提供一组默认实现)。
最简单的例子是一组比较器实现。
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());
        }
    };
}

"This 'pattern' can be applied to more complex scenarios, making full use of all the benefits that come with enums: iterating over instances, relying on their implicit order, retrieving an instance by name, and static methods providing the correct instance for specific contexts. All of this is hidden behind the interface, so your code will work with custom implementations without modification if you want something not available among the default options.
I have seen this successfully used to model time granularity (daily, weekly, etc.), where all the logic was encapsulated in an enum (choosing the right granularity for a given time range, specific behavior bound to each granularity as constant methods, etc.). However, the Granularity seen by the service layer was simply an interface."

4
您还可以添加CASE_INSENSITIVE { @Override public int compare(String s1, String s2) { return s1.compareToIgnoreCase(s2); } }。另一个尚未提到的优点是您可以获得强大的序列化支持;持久性表单仅包含类名和常量名,不依赖于比较器的任何实现细节。 - Holger

34
其他答案没有涵盖的使枚举特别强大的一个方面是能够拥有模板方法。方法可以作为基本枚举的一部分,并被每个类型覆盖。并且,由于行为附加到枚举上,通常消除了if-else结构或switch语句的需要,就像这篇博客文章演示的那样 - 其中enum.method()执行原来在条件语句中执行的操作。相同的示例还显示了使用枚举的静态导入,产生更清晰的DSL代码。

一些其他有趣的特性包括枚举提供equals()toString()hashCode()的实现,并实现SerializableComparable

对于枚举所提供的所有内容的完整介绍,我强烈推荐Bruce Eckel的Java编程思想(第4版),该书专门介绍了这个主题的一个章节。尤其是涉及Rock、Paper、Scissors(即RoShamBo)游戏作为枚举的示例,非常有启发性。


27

来自Java文档-

每当需要表示一组固定的常量时,您应该使用枚举类型。这包括自然枚举类型,例如我们太阳系中的行星和数据集,在编译时,您知道所有可能的值 - 例如,菜单上的选择,命令行标志等。

一个常见的例子是用一个枚举类型替换一个类,其中包含一组私有静态final int常量(在合理数量的常量范围内)。基本上,如果您认为在编译时了解“某些东西”的所有可能值,则可以将其表示为枚举类型。枚举提供了比包含常量的类更好的可读性和灵活性。

我能想到的枚举类型的其他几个优点。枚举类始终只有一个实例(因此使用枚举作为单例模式的概念)。另一个优点是您可以将枚举用作switch-case语句中的类型。您还可以使用toString()在枚举上将它们打印为可读字符串。


9
你会将366天的枚举用于代码中吗?因为这是一个固定的常量集合(366天不会改变),所以建议你这样做。但我建议你限制一下这个常量集合的大小。 - moinudin
3
通过巧妙的思考,可以将一周中的每一天和每个月份的天数总结在一个紧凑的枚举中。 - James P.
3
说到星期几... DayOfWeek 枚举类型现在已经预定义,内置于 Java 8 及其后续版本中,作为 java.time 框架的一部分(并且已经被回溯到 Java 6 & 7Android)。 - Basil Bourque
@moinudin 代表366天的原因不好的原因是因为它容易出现错误。如果用户在循环过程中没有考虑当前年份是否为闰年,会出现什么情况呢?但是,如果每一年都是366天,没有额外的小时、分钟或秒,我绝对会说为这366天中的每一天都设置一个枚举值是有用的,甚至是可取的。 - undefined
1
正是因为这个原因,才创建了DayOfWeek和Month枚举-因为它们是一致的,使用场景易于理解,很难弄错。不像366天。 - undefined

20
现在,我为什么要在日常编程中使用枚举类型?你可以使用枚举类型来表示一组小的固定常量或内部类模式,从而提高可读性。此外,当用于方法参数时,枚举类型可以强制执行某种严格性。它们还提供了有趣的可能性,例如在Oracle网站上的Planets示例中向构造函数传递信息,并且正如您发现的那样,也允许简单地创建单例模式。
例如:Locale.setDefault(Locale.US)Locale.setDefault(1)更易读,并通过在添加.分隔符时显示IDE中的固定值集合来强制使用所有整数。

15
枚举类型以自我记录的方式列出一组固定的值。
它们使您的代码更加明确,也更少出错。

为什么不使用Stringint代替常量中的Enum

  1. 编译器不允许打字错误,也不允许使用超出固定集合的值,因为枚举本身是类型。 结果:
    • 您不必编写前提条件(或手动if)来确保参数在有效范围内。
    • 类型不变式是免费的。
  2. 枚举可以像任何其他类一样具有行为。
  3. 无论如何,您可能需要类似数量的内存来使用String(这取决于Enum的复杂性)。

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

此外,它们在实例创建时(枚举加载时)确保了线程安全性,这在简化单例模式方面得到了广泛应用。 这篇博客阐述了一些其应用,例如用于解析器的状态机。

15

了解到 enums 就像其他类一样拥有常量字段和一个私有构造函数是很有用的。

例如,

public enum Weekday
{
  MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
} 
编译器将其编译如下;
class Weekday extends Enum
{
  public static final Weekday MONDAY  = new Weekday( "MONDAY",   0 );
  public static final Weekday TUESDAY = new Weekday( "TUESDAY ", 1 );
  public static final Weekday WEDNESDAY= new Weekday( "WEDNESDAY", 2 );
  public static final Weekday THURSDAY= new Weekday( "THURSDAY", 3 );
  public static final Weekday FRIDAY= new Weekday( "FRIDAY", 4 );
  public static final Weekday SATURDAY= new Weekday( "SATURDAY", 5 );
  public static final Weekday SUNDAY= new Weekday( "SUNDAY", 6 );

  private Weekday( String s, int i )
  {
    super( s, i );
  }

  // other methods...
}

15

枚举表示枚举,即逐个列举(一些事物)。

枚举是包含固定常量集的数据类型。

或者

枚举就像一个在编译时已知的固定实例集合的

例如:

public class EnumExample {
    interface SeasonInt {
        String seasonDuration();
    }

    private enum Season implements SeasonInt {
        // except the enum constants remaining code looks same as class
        // enum constants are implicitly public static final we have used all caps to specify them like Constants in Java
        WINTER(88, "DEC - FEB"), SPRING(92, "MAR - JUN"), SUMMER(91, "JUN - AUG"), FALL(90, "SEP - NOV");

        private int days;
        private String months;

        Season(int days, String months) { // note: constructor is by default private 
            this.days = days;
            this.months = months;
        }

        @Override
        public String seasonDuration() {
            return this+" -> "+this.days + "days,   " + this.months+" months";
        }

    }
    public static void main(String[] args) {
        System.out.println(Season.SPRING.seasonDuration());
        for (Season season : Season.values()){
            System.out.println(season.seasonDuration());
        }

    }
}

枚举类型的优点:

  • 在编译时检查,提高类型安全性,避免运行时错误。
  • 易于在 switch 中使用。
  • 可以遍历枚举值。
  • 可以拥有字段、构造函数和方法。
  • 可以实现多个接口,但不能扩展任何类,因为它内部扩展了 Enum 类。

详情请参见此链接


9

什么是枚举

  • 枚举是一个关键字,用于定义新的数据类型。应该大量使用类型安全的枚举。特别是,它们是一种强大的替代方案,可用于表示相关项集合的简单字符串或整数常量在较旧的API中。

为什么要使用枚举

  • 枚举是java.lang.Enum的隐式最终子类
  • 如果枚举是类的成员,则隐式为静态成员
  • 甚至在枚举类型本身内部,也永远不能使用new来创建枚举
  • name和valueOf只使用枚举常量的文本,而toString可以被覆盖以提供任何内容(如果需要)
  • 对于枚举常量,equals和==等同,可以互换使用
  • 枚举常量隐式为public static final

注意事项

  • 枚举类型无法扩展任何类。
  • 枚举类型不能作为一个超类。
  • 枚举常量出现的顺序被称为它们的“自然顺序”,并定义了其他项使用的顺序:compareTo、值的迭代顺序、EnumSet和EnumSet.range。
  • 枚举类型可以有构造器,静态和实例块、变量和方法,但不能有抽象方法。

8
你没有解释为什么要使用枚举,只是在不相关的标题下列出了几个枚举的属性。 - Duncan Jones
1
@DuncanJones:final和static这些关键字难道不已经表明了使用枚举的目的了吗? - tinker_fairy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接