枚举类型:为什么?何时使用?

7

我是一名即将毕业的计算机科学专业学生,在我的编程生涯中,我很少看到使用枚举的情况,除非是用于代表标准扑克牌的面值等经典案例。

你知道有哪些巧妙的方法可以在日常编码中使用枚举吗?

枚举为什么如此重要,在什么情况下应该确定构建枚举是最佳方法?


9
"你知道在日常编码中枚举类型有哪些巧妙的用法吗?" - 这不是关于巧妙性,而是关于拥有易读、易写、易维护、可重用等优秀特性的代码。enum/EnumMap/EnumSet组合能够出色地实现这些特性。抽象化是好的,enum是一种强大的抽象化类型,在Java中比在C#和C++中更加强大。 - polygenelubricants
@polygenelubricants,你能给我举一个使用EnumMap/EnumSet的实际例子吗? - danyim
@polyg:正如我之前所提到的,我不是Java方面的专家。为什么在Java中枚举比C++/C#更强大? - James Curran
2
@James:Java 枚举实例是真正的完整对象,可以拥有字段、构造函数和方法(甚至可以为每个实例不同地覆盖方法)。 - Michael Borgwardt
+1 给所有和我一样热爱枚举的人。 - whiskeysierra
7个回答

28

这些是针对 enumEnumMapEnumSet 的主要论据,通过简短的示例进行说明。

enum 辩护

截至Java 6,在使用 enum(以及其他改进)之后,java.util.Calendar 是混乱类的一个例子,可以从中受益匪浅。

当前,Calendar 定义了以下常量 (还有许多):

// int constant antipattern from java.util.Calendar
public static final int JANUARY = 0;
public static final int FEBRUARY = 1;
...
public static final int SUNDAY = 1;
public static final int MONDAY = 2;
...

这些都是int,尽管它们明显表示不同的概念实体。

以下是一些严重后果:

  • 它很脆弱,必须注意在需要时分配不同的数字。
    • 如果我们错误地设置MONDAY = 0;SUNDAY = 0;,那么就会有MONDAY == SUNDAY
  • 没有名称空间和类型安全性,因为一切都只是一个int
    • 我们可以setMonth(JANUARY),但我们也可以setMonth(THURSDAY)setMonth(42)
    • 谁知道set(int,int,int,int,int,int)(一个真正的方法!)做了什么!

相比之下,我们可以使用以下代码:

// Hypothetical enums for a Calendar library
enum Month {
   JANUARY, FEBRUARY, ...
}
enum DayOfWeek {
   SUNDAY, MONDAY, ...
}

现在我们再也不必担心MONDAY==SUNDAY(它永远不会发生!),并且由于MonthDayOfWeek是不同的类型,setMonth(MONDAY)无法编译。

此外,以下是一些改动前后的代码:

// BEFORE with int constants
for (int month = JANUARY; month <= DECEMBER; month++) {
   ...
}

在这里,我们做出了各种假设,例如JANUARY + 1 == FEBRUARY等。另一方面,enum的对应项更为简洁易读,且做出的假设更少(因此出错的机会更少):

// AFTER with enum
for (Month month : Month.values()) {
   ...
}

使用实例字段的理由

在Java中,enum是一个具有许多特殊属性的class,但仍然是一个class,允许您在需要时定义实例方法和字段。

考虑以下示例:

// BEFORE: with int constants
public static final int NORTH = 0;
public static final int EAST  = 1;
public static final int SOUTH = 2;
public static final int WEST  = 3;

public static int degreeFor(int direction) {
   return direction * 90; // quite an assumption!
   // must be kept in-sync with the int constants!
}

//...
for (int dir = NORTH; dir <= WEST; dir++) {
   ... degreeFor(dir) ...
}

另一方面,使用enum您可以编写如下内容:

enum Direction {
   NORTH(0), EAST(90), SOUTH(180), WEST(270);
   // so obvious! so easy to read! so easy to write! so easy to maintain!

   private final int degree;
   Direction(int degree)      { this.degree = degree; }
   public int getDegree()     { return degree; }
}

//...
for (Direction dir : Direction.values()) {
   ... dir.getDegree() ...
}

实例方法的理由

考虑以下示例:

static int apply(int op1, int op2, int operator) {
   switch (operator) {
      case PLUS  : return op1 + op2;
      case MINUS : return op1 - op2;
      case ...
      default: throw new IllegalArgumentException("Unknown operator!");
   }
}

如前面的例子所示,Java中的enum可以拥有实例方法,而且不仅如此,每个常量还可以拥有自己特定的@Override。以下代码演示了这一点:

enum Operator {
    PLUS  { int apply(int op1, int op2) { return op1 + op2; } },
    MINUS { int apply(int op1, int op2) { return op1 - op2; } },
    ...
    ;
    abstract int apply(int op1, int op2);
}

EnumMap的用法

以下是来自《Effective Java 2nd Edition》的一句话:

  

永远不要从枚举的 ordinal() 派生一个与其关联的值; 而是将它存储在实例字段中。(第31条:使用实例字段代替序数) 几乎不可能适当地使用 ordinal 来索引数组: 使用 EnumMap 替代。通常情况下,应用程序员很少甚至从不使用 Enum.ordinal。(第33条:使用 EnumMap 替代序数索引)

以前你可能会这样写:

// BEFORE, with int constants and array indexing
Employee[] employeeOfTheMonth = ...

employeeOfTheMonth[JANUARY] = jamesBond;

现在您可以拥有:

// AFTER, with enum and EnumMap
Map<Month, Employee> employeeOfTheMonth = ...

employeeOfTheMonth.put(Month.JANUARY, jamesBond);

为什么要使用EnumSet

在C++中,我们经常使用2的幂次方作为int常量来表示位集合,这依赖于按位运算。一个例子可能如下所示:

public static final int BUTTON_A = 1;
public static final int BUTTON_B = 2;
public static final int BUTTON_X = 4;
public static final int BUTTON_Y = 8;

int buttonState = BUTTON_A | BUTTON_X; // A & X are pressed!

if ((buttonState & BUTTON_B) != 0) {   // B is pressed...
   ...
}

使用enumEnumSet,这可能看起来像这样:

enum Button {
  A, B, X, Y;
}

Set<Button> buttonState = EnumSet.of(Button.A, Button.X); // A & X are pressed!

if (buttonState.contains(Button.B)) { // B is pressed...
   ...
}

参考资料

另请参阅

  • Effective Java 第二版
    • 第30条:使用enum代替int常量
    • 第31条:使用实例域代替序数
    • 第32条:使用EnumSet代替位域
    • 第33条:使用EnumMap代替序数索引

相关问题


我在最后有点匆忙,如果其他人认为还有需要改进的地方,我会根据反馈来修改这个回答。 - polygenelubricants
这篇回答已被指定为问题的采纳答案。你的帖子简洁明了,正是我所需要的。谢谢。 - danyim
1
在您的实例字段案例中,我相信您想写 case MINUS: return op1 - op2;(之前是加号操作符)。 - danyim
@danyim:是的,谢谢你指出来。我明天会修复它,并进行其他我能想到的编辑和改进。 - polygenelubricants

7
在Java 1.5之前,你将定义为String/int常量的内容现在应该定义为枚举类型。例如,不要再使用以下方式:
public class StatusConstants {
   public int ACTIVE = 1;
   public int SUSPENDED = 2;
   public int TEMPORARY_INACTIVE = 3;
   public int NOT_CONFIRMED = 4;
}

现在您拥有更安全和更开发者友好的:

public enum Status {
   ACTIVE, SUSPENDED, TEMPORARY_INACTIVE, NOT_CONFIRMED
}

同样适用于有多个选项的任何内容,比如状态、街道类型(str.、boul.、rd.等)、用户访问级别(管理员、版主、普通用户)等等。
点击此处了解更多示例和解释。

那不是一个孤立的情况吗,我有一个只包含定义常量的类? - danyim

5

考虑一些简单的代码:

没有枚举:

 int OPT_A_ON = 1;
 int OPT_A_OFF = 0;

 int OPT_B_ON = 1;
 int OPT_B_OFF = 0;

 SetOption(OPT_A_ON, OPT_B_OFF);    // Declaration: SetOption(int, int)

看起来不错,对吧?但是SetOptions()函数需要先传入选项B再传入选项A。虽然这段代码可以通过编译,但运行时会将选项设置反了。

现在,使用枚举类型:

  enum OptA {On =1, Off = 0};
  enum OptB {On =1, Off = 0};

  SetOption(OptA.On, OptB.Off);// Declaration: SetOption(OptB, OptA)

现在我们犯了同样的错误,但这一次,由于枚举是不同类型,所以编译器会捕捉到这个错误。

(注意:我并不是真正的Java程序员,所以请原谅小的语法错误)


你的示例中枚举语法有误(这不仅仅是一个小的语法错误):在Java中,enum值是完整的对象,而不仅仅是整数的花哨快捷方式,因此你不能像这样简单地定义它们的整数值。只需删除该赋值语句,你的代码就会正确。 - Joachim Sauer

3

我认为你说的是枚举,而不是枚举器。

枚举适用于应用程序中任何可能使用“魔术字符串”或“魔术数字”的地方。

最容易理解其用法的领域是文件访问。每种文件访问模式(读取、写入、追加)都在枚举中表示。如果您使用“魔术数字”,您可能会得到类似这样的东西:

public static class Constants
{
    public static final int FILEMODE_READ = 1;
    public static final int FILEMODE_WRITE = 2;
    public static final int FILEMODE_APPEND = 3;
}

当您可以使用枚举更清晰、简洁地表达意图时:

public enum FileMode
{
    Read,
    Write,
    Append
}

2

枚举类型非常适合表示某物的状态/状态。我经常使用以下枚举类型:

public enum PlayerState {
    WALKING, STANDING, JUMPING...
}

在枚举和派生类之间总是需要平衡。以一个简单的例子来说明,考虑一个Cat类。不同的Cat有不同的攻击性水平。因此,我们可以创建派生类HostileCatFriendlyCat等。然而,还有不同类型的猫,如LionsTigers等。突然间,我们就有了一堆Cat对象。因此,另一种解决方案是在Cat类中提供一个Hostility枚举,这样就减少了派生类的数量和整体复杂度。


感谢您的建议。那个例子确实帮助我理解了您所说的“平衡”。 - danyim

2

浏览Josh Bloch在Devoxx'08的Effective Java Reloaded演讲http://www.parleys.com/d/1411。他对在各种情况下使用枚举的重要性进行了出色的介绍。不幸的是,视频中缺少许多幻灯片,但这个演讲非常棒。 - Alain O'Dea

1
正如Manuel Selva建议的那样,阅读《Effective Java 第二版》,但也要阅读单例模式部分。使用枚举是拥有一个干净的单例对象最简单的方法。

昨天我第一次使用枚举单例。这样可以节省很多容易出错的样板代码。 - Alain O'Dea

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