枚举:为啥?啥时候?
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】:
这些是enum
、EnumMap
和EnumSet
的主要参数(通过简短的示例)。
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
(它永远不会发生!),而且由于Month
和DayOfWeek
是不同的类型,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...
...
使用enum
和EnumSet
,可能看起来像这样:
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
有不同程度的敌意。因此,我们可以创建派生类HostileCat
、FriendlyCat
等。但是,也有不同类型的猫,Lions
、Tigers
等。突然,我们有了一大堆Cat
对象。因此,另一种解决方案是在Cat
类中提供Hostility
枚举,这样可以减少派生类的数量和整体复杂性。
【讨论】:
感谢您的意见。这个例子真的让我明白了你所说的“平衡”是什么意思。【参考方案7】:正如 Manuel Selva 建议的那样,阅读 Effective Java Second Edition,但也要阅读单例部分。 使用枚举是拥有干净单例对象的最简单方法。
【讨论】:
我昨天第一次使用了枚举单例。节省了大量容易出错的样板文件。以上是关于枚举:为啥?啥时候?的主要内容,如果未能解决你的问题,请参考以下文章