枚举:为啥?啥时候?

Posted

技术标签:

【中文标题】枚举:为啥?啥时候?【英文标题】:Enumerations: Why? When?枚举:为什么?什么时候? 【发布时间】:2011-03-22 17:52:00 【问题描述】:

我是一名即将毕业的计算机科学专业的学生,​​在我的编码生涯中,我发现使用枚举的实例非常少,除了代表标准纸牌面的典型案例之外。

你知道在日常编码中使用枚举的任何巧妙方法吗?

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

【问题讨论】:

"你知道枚举在日常编码中的使用方法吗?" - 这与聪明无关;它是关于拥有可读、可写、可维护、可重用等的代码。enum/EnumMap/EnumSet 组合可以完美地实现所有这些。抽象很好,enum 是一个强大的抽象,在 Java 中比在 C#/C++ 中更是如此。 @polygenelubricants 你能告诉我一个使用 EnumMap/EnumSet 的实际例子吗? @polyg:正如我在下面提到的,我不是 Java 专家。为什么枚举在 Java 中比 C++/C# 更强大? @James:Java 枚举实例是真正成熟的对象,可以具有字段、构造函数和方法(您甚至可以为每个实例覆盖不同的方法)。 +1 给所有和我一样热爱枚举的人。 【参考方案1】:

这些是enumEnumMapEnumSet 的主要参数(通过简短的示例)。

enum的情况

从 Java 6 开始,java.util.Calendar 是一个杂乱无章的类示例,使用 enum(以及其他改进)可能会受益良多。

目前Calendar 定义了以下常量(among many others):

// 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() 派生与enum 关联的值;而是将其存储在实例字段中。 (第 31 条:使用实例字段而不是序数)使用序数来索引数组很少合适:使用 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的情况

经常使用两个int 常量的幂,例如在 C++ 中表示位集。这依赖于bitwise operations。示例可能如下所示:

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...
   ...

参考文献

Java Language Guide/Enums -- 非常完整的处理,有很多例子

另见

有效的 Java 第 2 版 第 30 项:使用 enum 代替 int 常量 第 31 项:使用实例字段而不是序数 第 32 项:使用EnumSet 代替位字段 第 33 项:使用EnumMap 代替序号索引

相关问题

Enum in Java. Advantages? Is there a a C-like way to get item number from enum in java ?——是的,ordinal(),不过…… Is it bad practice to use an Enum’s ordinal value to index an array in Java? -- 很可能,是的! [java] Number for each enum item? -- EnumSet 的主要候选人!

【讨论】:

我在接近尾声时有点匆忙;如果其他人觉得有些部分需要改进,我会根据反馈修改此答案。 这现在被指定为这个问题的可接受答案。你的帖子很简洁,正是我想要的。谢谢。 在您的实例字段案例中,我相信您的意思是写case MINUS : return op1 - op2;(您之前有一个+运算符) @danyim:是的,感谢您了解这一点。我会在明天修复它以及我能想到的其他编辑和改进。【参考方案2】:

在 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. 等)、用户访问级别(管理员、版主、普通用户)等。

查看this 了解更多示例和说明。

【讨论】:

这不是一个孤立的情况,我有一个只包含定义常量的类吗?【参考方案3】:

考虑一些简单的代码:

没有枚举:

 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 值是成熟的对象,而不仅仅是整数的花哨快捷方式,因此您可以'不要像这样简单地定义它们的整数值。只需删除该分配,您的代码就会正确。【参考方案4】:

我认为您在谈论枚举,而不是枚举数。

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

最容易理解其用途的领域是文件访问。每个文件访问模式(读、写、追加)都在枚举中表示。如果您使用的是“幻数”,您可能会得到如下所示的内容:

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

【讨论】:

【参考方案5】:

默认答案:所有 Java 开发人员都应该明确阅读 Effective Java Second Edition。有一章是关于枚举的

【讨论】:

浏览来自 Devoxx'08 parleys.com/d/1411 的 Josh Bloch 的 Effective Java Reloaded 演讲。他出色地介绍了在各种场景中使用枚举的重要性。不幸的是,视频中缺少许多幻灯片,但演讲很棒。【参考方案6】:

枚举非常适合表示某事物的状态/状态。我经常使用枚举,例如:

public enum PlayerState 
    WALKING, STANDING, JUMPING...

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

【讨论】:

感谢您的意见。这个例子真的让我明白了你所说的“平衡”是什么意思。【参考方案7】:

正如 Manuel Selva 建议的那样,阅读 Effective Java Second Edition,但也要阅读单例部分。 使用枚举是拥有干净单例对象的最简单方法

【讨论】:

我昨天第一次使用了枚举单例。节省了大量容易出错的样板文件。

以上是关于枚举:为啥?啥时候?的主要内容,如果未能解决你的问题,请参考以下文章

三歪问我为啥用枚举,枚举有哪些用法?

java里啥情况用枚举啊

java中的枚举是啥意思?

java中的枚举是啥意思?

Java里的包,类,接口,枚举是啥意思

JAVA中枚举是啥意思,怎么用