Java 枚举 enum 详解

时间:2024-09-17 15:36:02

本文部分摘自 On Java 8

枚举类型

Java5 中添加了一个 enum 关键字,通过 enum 关键字,我们可以将一组拥有具名的值的有限集合创建为一种新的类型,这些具名的值可以作为常规的程序组件使用,例如:

public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
}

这里创建了一个名为 Spiciness 的枚举类型,它有 5 个值。由于枚举类型的实例是常量,因此按照命名惯例,它们都用大写字母表示(如果名称中含有多个单词,使用下划线分隔)

要使用 enum,需要创建一个该类型的引用,然后将其赋值给某个实例:

public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
}
// 输出:MEDIUM

在 switch 中使用 enum,是 enum 提供的一项非常便利的功能。一般来说,在 switch 中只能使用整数值,而枚举实例天生就具备整数值的次序,并且可以通过 ordinal() 方法取得其次序,因此我们可以在 switch 语句中使用 enum

一般情况下我们必须使用 enum 类型来修饰一个 enum 实例,但是在 case 语句中却不必如此。下面的例子使用 enum 构造了一个模拟红绿灯状态变化:

enum Signal { GREEN, YELLOW, RED, }

public class TrafficLight {

    Signal color = Signal.RED;

    public void change() {
switch(color) {
case RED: color = Signal.GREEN;
break;
case GREEN: color = Signal.YELLOW;
break;
case YELLOW: color = Signal.RED;
break;
}
} @Override
public String toString() {
return "The traffic light is " + color;
} public static void main(String[] args) {
TrafficLight t = new TrafficLight();
for(int i = 0; i < 7; i++) {
System.out.println(t);
t.change();
}
}
}

枚举的基本特性

Java 中的每一个枚举都继承自 java.lang.Enum 类,所有枚举实例都可以调用 Enum 类的方法

调用 enum 的 values() 方法,返回 enum 实例的数组,而且该数组中的元素严格保持其在 enum 中声明时的顺序,因此你可以在循环中使用 values() 返回的数组

enum Shrubbery { GROUND, CRAWLING, HANGING }
public class EnumClass {
public static void main(String[] args) {
for(Shrubbery s : Shrubbery.values()) {
System.out.println(s);
// 返回每个枚举实例在声明时的次序
System.out.println(s.ordinal());
// 返回与此枚举常量的枚举类型相对应的 Class 对象
System.out.println(s.getDeclaringClass());
// 返回枚举实例声明时的名字,效果等同于直接打印
System.out.println(s.name());
...
}
}
}
// 输出:
// GROUND
// 0
// GROUND
// CRAWLING
// 1
// CRAWLING
// HANGING
// 2
// HANGING

可以使用 == 来比较 enum 实例,编译器会自动为你提供 equals() 和 hashCode() 方法。同时,Enum 类实现了 Comparable 接口,所以它具有 compareTo() 方法,同时,它还实现了 Serializable 接口

ValueOf() 方法是在 Enum 中定义的 static 方法,根据给定的名字返回相应的 enum 实例,如果不存在给定名字的实例,将抛出异常

Shrubbery shrub = Enum.valueOf(Shrubbery.class, "HANGING");

我们再来看看 values() 方法,为什么要说这个呢?前面提到,编译器为你创建的 enum 类都继承自 Enum 类。然而,如果你研究一下 Enum 类就会发现,它并没有 values() 方法。可我们明明已经用过该方法了呀,难道是这个方法被藏起来了?答案是,values() 是由编译器添加的 static 方法,编译器还会为创建的枚举类标记为 static final,所以无法被继承

由于 values() 方法是由编译器插入到 enum 定义中的 static 方法,所以,如果你将 enum 实例向上转型为 Enum,那么 values() 方法就不可用了。不过,在 Class 中有一个 getEnumConstants() 方法,所以即便 Enum 接口中没有 values() 方法,我们仍然可以通过 Class 对象取得所有 enum 实例

enum Search { HITHER, YON }
public class UpcastEnum {
public static void main(String[] args) {
for(Enum en : e.getClass().getEnumConstants())
System.out.println(en);
}
}

因为 getEnumConstants() 是 Class 上的方法,所以你甚至可以对不是枚举的类调用此方法,只不过,此时该方法返回 null

方法添加

除了不能继承自一个 enum 之外,我们基本上可以将 enum 看作一个常规的类。也就是说我们可以向 enum 中添加方法。enum 甚至可以有 main() 方法

我们希望每个枚举实例能够返回对自身的描述,而不仅仅只是默认的 toString() 实现,这只能返回枚举实例的名字。为此,你可以提供一个构造器,专门负责处理这个额外的信息,然后添加一个方法,返回这个描述信息。看一看下面的示例:

public enum OzWitch {

    WEST("Miss Gulch, aka the Wicked Witch of the West"),
NORTH("Glinda, the Good Witch of the North"),
EAST("Wicked Witch of the East, wearer of the Ruby "),
SOUTH("Good by inference, but missing"); // 必须在 enum 实例序列的最后添加一个分号 private String description; private OzWitch(String description) {
this.description = description;
} public String getDescription() { return description; } public static void main(String[] args) {
for(OzWitch witch : OzWitch.values())
System.out.println(witch + ": " + witch.getDescription());
}
}

在这个例子中,我们有意识地将 enum 的构造器声明为 private,但对于它的可访问性而言,并没有什么变化,因为(即使不声明为 private)我们只能在 enum 定义的内部使用其构造器创建 enum 实例。一旦 enum 的定义结束,编译器就不允许我们再使用其构造器来创建任何实例了

如果希望覆盖 enum 中的方法,例如覆盖 toString() 方法,与覆盖一般类的方法没有区别:

public enum SpaceShip {

    SCOUT, CARGO, TRANSPORT,
CRUISER, BATTLESHIP, MOTHERSHIP; @Override
public String toString() {
String id = name();
String lower = id.substring(1).toLowerCase();
return id.charAt(0) + lower;
} public static void main(String[] args) {
Stream.of(values()).forEach(System.out::println);
}
}

实现接口

我们已经知道,所有的 enum 都继承自 Java.lang.Enum 类。由于 Java 不支持多重继承,所以 enum 不能再继承其他类,但可以实现一个或多个接口

enum CartoonCharacter implements Supplier<CartoonCharacter> {

    SLAPPY, SPANKY, PUNCHY,
SILLY, BOUNCY, NUTTY, BOB; private Random rand = new Random(47); @Override
public CartoonCharacter get() {
return values()[rand.nextInt(values().length)];
}
}

通过实现一个供给型接口,就可以通过调用 get() 方法得到一个随机的枚举值。我们可以借助泛型,使随机选择这个工作更加一般化,成为一个通用的工具类

public class Enums {

    private static Random rand = new Random(47);

    public static <T extends Enum<T>> T random(Class<T> ec) {
return random(ec.getEnumConstants());
} public static <T> T random(T[] values) {
return values[rand.nextInt(values.length)];
}
}

<T extends Enum> 表示 T 是一个 enum 实例。而将 Class 作为参数的话,我们就可以利用 Class 对象得到 enum 实例的数组了。重载后的 random() 方法只需使用 T[] 作为参数,因为它并不会调用 Enum 上的任何操作,它只需从数组中随机选择一个元素即可。这样,最终的返回类型正是 enum 的类型

使用接口可以帮助我们实现将枚举元素分类的目的,举例来说,假设你想用 enum 来表示不同类别的食物,同时还希望每个 enum 元素仍然保持 Food 类型,那可以这样实现:

public interface Food {

    enum Appetizer implements Food {
SALAD, SOUP, SPRING_ROLLS;
} enum MainCourse implements Food {
LASAGNE, BURRITO, PAD_THAI,LENTILS, HUMMOUS, VINDALOO;
} enum Dessert implements Food {
TIRAMISU, GELATO, BLACK_FOREST_CAKE,FRUIT, CREME_CARAMEL;
} enum Coffee implements Food {
BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,LATTE, CAPPUCCINO, TEA, HERB_TEA;
}
}

对于 enum 而言,实现接口是使其子类化的唯一办法,所以嵌入在 Food 中的每个 enum 都实现了 Food 接口。现在,在下面的程序中,我们可以说“所有东西都是某种类型的 Food"

public class TypeOfFood {

    public static void main(String[] args) {

        Food food = Appetizer.SALAD;
food = MainCourse.LASAGNE;
food = Dessert.GELATO;
food = Coffee.CAPPUCCINO;
}
}

如果 enum 的数量太多,那么一个接口中的代码量可能就很大。我们可以利用 getEnumConstants() 方法,根据某个 Class 对象取得某个 Food 子类的所有 enum 实例

public enum Course {

    APPETIZER(Food.Appetizer.class),
MAINCOURSE(Food.MainCourse.class),
DESSERT(Food.Dessert.class),
COFFEE(Food.Coffee.class); private Food[] values; private Course(Class<? extends Food> kind) {
values = kind.getEnumConstants();
} // 随机获取 Food 子类的某个 enum 实例
public Food randomSelection() {
return Enums.random(values);
}
}

如果对内部类熟悉的话,我们还可以使用更加简洁的管理枚举的方式,就是将第一种方式中的接口嵌套在枚举里,使得代码具有更清晰的结构

enum SecurityCategory {

    STOCK(Security.Stock.class),
BOND(Security.Bond.class); Security[] values; SecurityCategory(Class<? extends Security> kind) {
values = kind.getEnumConstants();
} interface Security {
enum Stock implements Security {
SHORT, LONG, MARGIN
}
enum Bond implements Security {
MUNICIPAL, JUNK
}
} public Security randomSelection() {
return Enums.random(values);
} public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
SecurityCategory category = Enums.random(SecurityCategory.class);
System.out.println(category + ": " + category.randomSelection());
}
}
}

EnumSet

EnumSet 是一个用来操作 Enum 的集合,可以用来存放属于同一枚举类型的枚举常量,其中元素存放的次序决定于 enum 实例定义时的次序。EnumSet 的设计初衷是为了替代传统的基于 int 的“位标志”。传统的“位标志”可以用来表示某种“开/关”信息,不过,使用这种标志,我们最终操作的只是一些 bit,而不是这些 bit 想要表达的概念,因此很容易写出令人费解的代码

既然 EnumSet 要替代 bit 标志,那么它的性能应该要做到与使用 bit 一样高效才对。EnumSet 的基础是 long,一个 long 值有 64 位,一个 enum 实例只需一位 bit 表示其是否存在。 也就是说,在不超过一个 long 的表达能力的情况下,你的 EnumSet 可以应用于最多不超过 64 个元素的 enum。如果 enum 超过了 64 个元素,EnumSet 会在必要时增加一个 long

EnumSet 的方法如下:

方法 作用
allOf(Class elementType) 创建一个包含指定枚举类里所有枚举值的 EnumSet 集合
complementOf(EnumSet e) 创建一个元素类型与指定 EnumSet 里元素类型相同的 EnumSet 集合,新 EnumSet 集合包含原 EnumSet 集合所不包含的的枚举值
copyOf(Collection c) 使用一个普通集合来创建 EnumSet 集合
copyOf(EnumSet e) 创建一个指定 EnumSet 具有相同元素类型、相同集合元素的 EnumSet 集合
noneOf(Class elementType) 创建一个元素类型为指定枚举类型的空 EnumSet
of(E first,E…rest) 创建一个包含一个或多个枚举值的 EnumSet 集合,传入的多个枚举值必须属于同一个枚举类
range(E from,E to) 创建一个包含从 from 枚举值到 to 枚举值范围内所有枚举值的 EnumSet 集合

示例代码:

public enum AlarmPoints {
STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3,
OFFICE4, BATHROOM, UTILITY, KITCHEN
} public class EnumSets {
public static void main(String[] args) {
EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class);
points.add(BATHROOM);
System.out.println(points);
points.addAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
System.out.println(points);
points = EnumSet.allOf(AlarmPoints.class);
points.removeAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
System.out.println(points);
points.removeAll(EnumSet.range(OFFICE1, OFFICE4));
System.out.println(points);
points = EnumSet.complementOf(points);
System.out.println(points);
}
}

EnumMap

EnumMap 是一种特殊的 Map,它要求 key 必须为枚举类型,value 没有限制,底层由双数组实现(一个存放 key,另一个存放 value),同时,当 value 为 null 时会特殊处理为一个 Object 对象,和 EnumSet 一样,元素存放的次序决定于 enum 实例定义时的次序

// key 类型
private final Class<K> keyType; // key 数组
private transient K[] keyUniverse; // value 数组
private transient Object[] vals; // 键值对个数
private transient int size = 0; // value 为 null 时对应的值
private static final Object NULL = new Object() {
public int hashCode() {
return 0;
}
public String toString() {
return "java.util.EnumMap.NULL";
}
};

由于 EnumMap 可以存放枚举类型,所以初始化时必须指定枚举类型,EnumMap 提供了三个构造函数

// 使用指定的键类型创建一个空的枚举映射
EnumMap(Class<K> keyType);
// 创建与指定的枚举映射相同的键类型的枚举映射,最初包含相同的映射(如果有)
EnumMap(EnumMap<K,? extends V> m);
// 创建从指定映射初始化的枚举映射
EnumMap(Map<K,? extends V> m);

除此之外,EnumMap 与普通 Map 在操作上没有区别,EnumMap 的优点在于允许程序员改变值对象,而常量相关的方法在编译期就被固定了

常量特定方法

我们可以为 enum 定义一个或多个 abstract 方法,然后为每个 enum 实例实现该抽象方法

public enum ConstantSpecificMethod {
DATE_TIME {
@Override
String getInfo() {
return DateFormat.getDateInstance().format(new Date());
}
},
CLASSPATH {
@Override
String getInfo() {
return System.getenv("CLASSPATH");
}
},
VERSION {
@Override
String getInfo() {
return System.getProperty("java.version");
}
};
abstract String getInfo();
public static void main(String[] args) {
for(ConstantSpecificMethod csm : values())
System.out.println(csm.getInfo());
}
}

在面向对象的程序设计中,不同的行为与不同的类关联。通过常量相关的方法,每个 enum 实例可以具备自己独特的行为,这似乎说明每个 enum 实例就像一个独特的类。在上面的例子中,enum 实例似乎被当作其超类 ConstantSpecificMethod 来使用,在调用 getInfo() 方法时,体现出多态的行为

然而,enum 实例与类的相似之处也仅限于此了。我们并不能真的将 enum 实例作为一个类型来使用,因为每一个 enum 元素都是指定枚举类型的 static final 实例

除了 abstract 方法外,程序员还可以覆盖普通方法


public enum OverrideConstantSpecific {
NUT, BOLT,
WASHER {
@Override
void f() {
System.out.println("Overridden method");
}
};
void f() {
System.out.println("default behavior");
}
public static void main(String[] args) {
for(OverrideConstantSpecific ocs : values()) {
System.out.print(ocs + ": ");
ocs.f();
}
}
}

多路分发

现在有一个数学表达式 Number.plus(Number),我们知道 Number 是各种数字对象的超类,假设有 a 和 b 两个 Number 类型的对象,根据上述的表达式代入得 a.plus(b),但你现在只知道 a、b 属于 Number 类型,具体是什么数字你并不知道,有可能是整数、浮点数,根据不同的数字类型,执行数学操作后的结果应该也不一样才对,怎么让它们正确地交互呢?

如果你了解 Java 多态就知道,Java 多态的实现依赖的是 Java 的动态绑定机制,在运行时发现对象的真实类型。但 Java 只支持单路分发,就是说如果要执行的操作包含不止一个类型未知的对象,那么 Java 的动态绑定机制只能处理其中一个类型,a.plus(b) 涉及到两个类型,自然无法解决我们的问题,所以我们必须自己来判定其他类型

解决问题的方法就是多路分发,上面的例子,由于只有两个分发,一般称为两路分发。多态只能发生在方法调用时,所以,如果你想使用两路分发,那么就必须有两个方法调用:第一个方法调用决定第一个未知类型,第二个方法调用决定第二个未知的类型。程序员必须设定好某种配置,以便一个方法调用能够引出其他方法调用,从而在这个过程中处理多种类型

来看一个猜拳的例子:

package enums;
public enum Outcome { WIN, LOSE, DRAW } // 猜拳的结果:胜、负、平手
package enums;
import java.util.*;
import static enums.Outcome.*; // 引入 enums,这样就不用写前缀 Outcome 了
interface Item {
Outcome compete(Item it);
Outcome eval(Paper p);
Outcome eval(Scissors s);
Outcome eval(Rock r);
}
class Paper implements Item {
@Override
public Outcome compete(Item it) {
return it.eval(this);
}
@Override
public Outcome eval(Paper p) { return DRAW; }
@Override
public Outcome eval(Scissors s) { return WIN; }
@Override
public Outcome eval(Rock r) { return LOSE; }
@Override
public String toString() { return "Paper"; }
}
class Scissors implements Item {
@Override
public Outcome compete(Item it) {
return it.eval(this);
}
@Override
public Outcome eval(Paper p) { return LOSE; }
@Override
public Outcome eval(Scissors s) { return DRAW; }
@Override
public Outcome eval(Rock r) { return WIN; }
@Override
public String toString() { return "Scissors"; }
}
class Rock implements Item {
@Override
public Outcome compete(Item it) {
return it.eval(this);
}
@Override
public Outcome eval(Paper p) { return WIN; }
@Override
public Outcome eval(Scissors s) { return LOSE; }
@Override
public Outcome eval(Rock r) { return DRAW; }
@Override
public String toString() { return "Rock"; }
}
public class RoShamBo1 {
static final int SIZE = 20;
private static Random rand = new Random(47);
public static Item newItem() {
switch(rand.nextInt(3)) {
default:
case 0: return new Scissors();
case 1: return new Paper();
case 2: return new Rock();
}
}
public static void match(Item a, Item b) {
System.out.println(
a + " vs. " + b + ": " + a.compete(b));
}
public static void main(String[] args) {
for(int i = 0; i < SIZE; i++)
match(newItem(), newItem());
}
}

上面就是多路分发的实现,它的好处在于避免判定多个对象的类型的冗余代码,不过配置过程需要很多道工序。我们既然学习了枚举,自然可以考虑用枚举对代码进行优化

public interface Competitor<T extends Competitor<T>> {
Outcome compete(T competitor);
} public class RoShamBo { public static <T extends Competitor<T>> void match(T a, T b) {
System.out.println(a + " vs. " + b + ": " + a.compete(b));
} public static <T extends Enum<T> & Competitor<T>>
void play(Class<T> rsbClass, int size) {
for(int i = 0; i < size; i++)
match(Enums.random(rsbClass),Enums.random(rsbClass));
}
} public enum RoShamBo2 implements Competitor<RoShamBo2> { PAPER(DRAW, LOSE, WIN),
SCISSORS(WIN, DRAW, LOSE),
ROCK(LOSE, WIN, DRAW); private Outcome vPAPER, vSCISSORS, vROCK; RoShamBo2(Outcome paper, Outcome scissors, Outcome rock) {
this.vPAPER = paper;
this.vSCISSORS = scissors;
this.vROCK = rock;
} @Override
public Outcome compete(RoShamBo2 it) {
switch(it) {
default:
case PAPER: return vPAPER;
case SCISSORS: return vSCISSORS;
case ROCK: return vROCK;
}
} public static void main(String[] args) {
RoShamBo.play(RoShamBo2.class, 20);
}
}

也可以使用将 enum 用在 switch 语句中

import static enums.Outcome.*;
public enum RoShamBo3 implements Competitor<RoShamBo3> {
PAPER {
@Override
public Outcome compete(RoShamBo3 it) {
switch(it) {
default:
case PAPER: return DRAW;
case SCISSORS: return LOSE;
case ROCK: return WIN;
}
}
},
SCISSORS {
@Override
public Outcome compete(RoShamBo3 it) {
switch(it) {
default:
case PAPER: return WIN;
case SCISSORS: return DRAW;
case ROCK: return LOSE;
}
}
},
ROCK {
@Override
public Outcome compete(RoShamBo3 it) {
switch(it) {
default:
case PAPER: return LOSE;
case SCISSORS: return WIN;
case ROCK: return DRAW;
}
}
};
@Override
public abstract Outcome compete(RoShamBo3 it);
public static void main(String[] args) {
RoShamBo.play(RoShamBo3.class, 20);
}
}

对上述代码还可以再压缩一下

public enum RoShamBo4 implements Competitor<RoShamBo4> {
ROCK {
@Override
public Outcome compete(RoShamBo4 opponent) {
return compete(SCISSORS, opponent);
}
},
SCISSORS {
@Override
public Outcome compete(RoShamBo4 opponent) {
return compete(PAPER, opponent);
}
},
PAPER {
@Override
public Outcome compete(RoShamBo4 opponent) {
return compete(ROCK, opponent);
}
};
Outcome compete(RoShamBo4 loser, RoShamBo4 opponent) {
return ((opponent == this) ? Outcome.DRAW
: ((opponent == loser) ? Outcome.WIN
: Outcome.LOSE));
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo4.class, 20);
}
}

使用 EnumMap 能够实现真正的两路分发。EnumMap 是为 enum 专门设计的一种性能非常好的特殊 Map。由于我们的目的是摸索出两种未知的类型,所以可以用一个 EnumMap 的 EnumMap 来实现两路分发:

package enums;
import java.util.*;
import static enums.Outcome.*;
enum RoShamBo5 implements Competitor<RoShamBo5> {
PAPER, SCISSORS, ROCK;
static EnumMap<RoShamBo5,EnumMap<RoShamBo5,Outcome>>
table = new EnumMap<>(RoShamBo5.class);
static {
for(RoShamBo5 it : RoShamBo5.values())
table.put(it, new EnumMap<>(RoShamBo5.class));
initRow(PAPER, DRAW, LOSE, WIN);
initRow(SCISSORS, WIN, DRAW, LOSE);
initRow(ROCK, LOSE, WIN, DRAW);
}
static void initRow(RoShamBo5 it,
Outcome vPAPER, Outcome vSCISSORS, Outcome vROCK) {
EnumMap<RoShamBo5,Outcome> row =
RoShamBo5.table.get(it);
row.put(RoShamBo5.PAPER, vPAPER);
row.put(RoShamBo5.SCISSORS, vSCISSORS);
row.put(RoShamBo5.ROCK, vROCK);
}
@Override
public Outcome compete(RoShamBo5 it) {
return table.get(this).get(it);
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo5.class, 20);
}
}

我们还可以进一步简化实现两路分发的解决方案。我们注意到,每个 enum 实例都有一个固定的值(基于其声明的次序),并且可以通过 ordinal() 方法取得该值。因此我们可以使用二维数组,将竞争者映射到竞争结果。采用这种方式能够获得最简洁、最直接的解决方案

package enums;
import static enums.Outcome.*;
enum RoShamBo6 implements Competitor<RoShamBo6> {
PAPER, SCISSORS, ROCK;
private static Outcome[][] table = {
{ DRAW, LOSE, WIN }, // PAPER
{ WIN, DRAW, LOSE }, // SCISSORS
{ LOSE, WIN, DRAW }, // ROCK
};
@Override
public Outcome compete(RoShamBo6 other) {
return table[this.ordinal()][other.ordinal()];
}
public static void main(String[] args) {
RoShamBo.play(RoShamBo6.class, 20);
}
}