目录
枚举的基本特性
枚举类型中的自定义方法
switch语句中的枚举
编译器创建的values()方法
使用实现代替继承
构建工具:生成随机的枚举
组织枚举
EnumSet
EnumMap
本笔记参考自: 《On Java 中文版》
枚举类型通过enum关键字定义,其中包含了数量有限的命名变量。
枚举的基本特性
当我们创建枚举类型时,系统会自动为我们生成一个辅助类,这个类继承自java.lang.Enum。下面的例子展示了其中的一些方法:
【例子:枚举自带的方法】
enum Fruit {
APPLE,
BANANA,
ORANGE
}
public class EnumClass {
public static void main(String[] args) {
for (Fruit f : Fruit.values()) {
System.out.println(
f + "对应序数:" + f.ordinal());
System.out.println(
"compareTo(BANANA):" + f.compareTo(Fruit.BANANA));
System.out.println(
"equals(BANANA):" + f.equals(Fruit.BANANA));
System.out.println("f == Fruit.ORANGE? " +
(f == Fruit.ORANGE));
System.out.println(f.getDeclaringClass());
System.out.println(f.name());
System.out.println("====================");
}
}
}
程序执行的结果是:
简单介绍一些方法:
- values():生成一个由枚举常量组成的数组,其中常量的顺序和常量声明的顺序保持一致。
- ordinal():返回一个从0开始的int值,代表每个枚举实例的声明顺序。
- getDeclaringClass():获得该枚举实例所属的外部包装类。
- name():返回枚举实例被声明的名称。
equals()方法由编译器自动生成,而compareTo()方法则来自Comparable接口(Enum实现了它),这里不再赘述。
静态导入枚举类型
使用枚举类型的理由之一,就是枚举可以增强我们代码的可读性。有时,我们会使用静态导入枚举的方式使用枚举:
【例子:静态导入的枚举类型】
首先创建一个枚举类型:
// 关于香料的枚举:
public enum SpicinessEnum {
NOT, MILD, MEDIUM, HOT, FLAMING
}
现在,让我们在程序中静态导入它:
// 静态导入一个枚举类型:
import static enums.SpicinessEnum.*;
// 制作一个玉米煎饼:
public class Burrito2 {
SpicinessEnum degree;
public Burrito2(SpicinessEnum degree) {
this.degree = degree;
}
@Override
public String toString() {
return "来个玉米饼,添加香料:" + degree;
}
public static void main(String[] args) {
System.out.println(new Burrito2(NOT));
System.out.println(new Burrito2(MEDIUM));
System.out.println(new Burrito2(HOT));
}
}
程序执行的结果是:
通过static import,我们将所有的枚举实例标识符都引入了本地命名空间。值得一提的是,是否静态导入枚举类型大多不会影响代码的运行,但我们仍需要考虑代码的可读性:若代码本身很复杂,静态导入或许就不会是一个更好的选择。
若枚举定义在通过文件,或定义在默认包中,则无法使用上述的这种方式。
枚举类型中的自定义方法
除去无法继承,基本上可以将枚举类型看做一个普通的类。可以向其中添加自定义方法,甚至于main()方法。
通过创建一个含参构造器,枚举可以获取额外的信息,并通过额外的方法来扩展应用。例如:
【例子:在枚举中创建新的方法】
public enum MakeAHuman {
HEAD("我来组成头部"),
BODY("我来组成躯体"),
TONSIL("我来组成扁桃体");
private String description;
private MakeAHuman(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
// 也可以进行方法重载:
@Override
public String toString() {
String id = name();
String lower = id.substring(1).toLowerCase();
return id.charAt(0) + lower;
}
public static void main(String[] args) {
for (MakeAHuman human : MakeAHuman.values())
System.out.println(human +
": " + human.getDescription());
}
}
程序执行的结果是:
若想要添加自定义方法,首先必须用分号结束枚举实例的序列。注意:Java会强制我们在枚举中先定义实例。
之前也提到过,枚举类型不允许继承。这意味着枚举在被定义完毕后,无法被用于创建任何新的类型。
switch语句中的枚举
枚举类型可以被应用于switch语句。一般,switch语句只支持整型或字符串类型,但ordinal()方法可以获取枚举内部的整型序列。在这里,编译器完成了后台的各种工作。
在通常情况下,若要使用枚举实例,就需要使用枚举的类型名对其进行限定。但在switch语句中不需要这么做:
【例子:switch中的枚举】
enum Signal {
GREEN, YELLOW, RED
}
public class TrafficLight {
Signal color = Signal.RED;
public void change() {
// 在case语句中,无需使用Signal进行限定:
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 "现在,信号灯的颜色是:" + color;
}
public static void main(String[] args) {
TrafficLight t = new TrafficLight();
for (int i = 0; i < 7; i++) {
System.out.println(t);
t.change();
}
}
}
程序执行的结果是:
尽管没有添加default语句,但编译器也没有报错。这不见得是一件好事,因为即使我们注释掉了其中的一条分支,编译器也不会报错:
因此,在编写分支语句时我们必须小心,确保代码已经覆盖了所有的分支。
编译器创建的values()方法
根据官方文档的描述,Enum类中并不存在values()方法:
因此我们可以猜测,编译器在后台为我们完成了某件事。接下来的例子会通过反射分析Enum类中的方法:
【例子:通过反射探究Enum类】
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.TreeSet;
enum Explore {
HERE,
THERE
}
public class Reflection {
public static Set<String> analyze(
Class<?> enumClass) {
System.out.println("_____分析" + enumClass + "_____");
System.out.println("$接口:");
for (Type t : enumClass.getGenericInterfaces())
System.out.println(t);
System.out.println("$基类:" +
enumClass.getSuperclass());
System.out.println("$方法:");
Set<String> methods = new TreeSet<>();
for (Method m : enumClass.getMethods())
methods.add(m.getName());
System.out.println(methods);
return methods;
}
public static void main(String[] args) {
Set<String> exploreMethods =
analyze(Explore.class);
System.out.println();
Set<String> enumMethods =
analyze(Enum.class);
System.out.println();
System.out.println("Explore.containsAll(Enum)? " +
exploreMethods.containsAll(enumMethods));
System.out.print("Explore.removeAll(Enum): ");
exploreMethods.removeAll(enumMethods);
System.out.println("[" + exploreMethods + "]");
}
}
程序执行的结果是:
从反射的结果可以发现,枚举Explore中多出了Enum没有的values()方法。现在让我们进一步,通过反编译Explore来查看它的内部信息:
反编译告诉我们,values()方法是由编译器添加的一个静态方法。除此之外,还可以发现:Explore内部的valueOf()方法只有一个参数,而Enum自带的valueOf()却有两个参数:
然而,Set只会关注方法名,因此在执行Explore.remove(Enum)后,valueOf()方法也被去除了。
从反射的结果中,我们还可以知道两件事:① Explore枚举是final类,因此我们无法继承;②反射认为Explore的基类就是Enum,这并不准确,Explore的基类应该是Enum<Explore>,类型擦除使得编译器无法获取完整类型信息,因此才会出现这种现象。
需要注意的一点是,values()只在子类Explore中存在(由编译器插入),因此当我们将该枚举类型向上转型为Enum时。我们将无法使用这个方法。作为替代,可以使用Class.getEnumConstants():
【例子:getEnumConstants()的使用例】
enum Search {
HITHER,
YON
}
public class UpcastEnum {
public static void main(String[] args) {
Search[] vals = Search.values();
Enum e = Search.HITHER; // 发生向上转型
// e.values(); // 此时会发现Enum中并没有values()方法
// Class.getEnumConstants()方法返回一个包含枚举中的每个元素的数组
for (Enum en : e.getClass().getEnumConstants())
System.out.println(en);
}
}
程序执行的结果是:
因为getEnumConstants()方法属于Class类,因此非枚举类型也可以调用它。不过此时方法会返回null,调用这个结果会抛出异常。
使用实现代替继承
已知,所有枚举类都会默认继承java.lang.Enum。而Java不支持多重继承,这就意味着一个枚举类无法再继承任何其他的类:
// enum NotPossible extends SomethingElse { ... // 不允许这么做
作为替代,我们可以令枚举类型实现一些接口:
【例子:让枚举类实现接口】
import java.util.Random;
import java.util.function.Supplier;
enum LetterCharacter
implements Supplier<LetterCharacter> {
A, B, C, D, E, F, G;
private Random rand =
new Random(47);
@Override
public LetterCharacter get() {
return values()[rand.nextInt(values().length)];
}
}
public class EnumImplementation {
public static <T> void printNext(Supplier<T> rg) {
System.out.print(rg.get() + " ");
}
public static void main(String[] args) {
LetterCharacter ll = LetterCharacter.G;
for (int i = 0; i < 10; i++)
printNext(ll);
}
}
程序执行的结果是:
这种做法有一点很奇怪:我们必须传入一个枚举实例,然后才能使用printNext()方法。
构建工具:生成随机的枚举
为了方便我们使用枚举,可以创建一个用于随机生成枚举的Enums类:
package onjava;
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)];
}
}
这个类中的random()方法会接收Class对象,并返回随机的枚举对象。
(因为之后也会使用该类,因此在这里提前进行展示)
组织枚举
尽管枚举类型无法继承,但我们任然会有可能用到继承关系的情况。一般地,继承枚举有两个动机:
- 希望扩充原始枚举中的元素。
- 想要使用子类型创建不同的子分组。
一个方法是通过接口对枚举进行分类。下面的例子在接口中将元素分类完毕,然后会基于这个接口生成一个枚举,这样就能实现分类的目的:
【例子:在接口中分类】
public interface Food {
enum Appetizer implements Food {
SALAD, SOUP, SPRING_ROLLS;
}
enum MainCourse implements Food {
RICE, NOODLES, BREAD, PASTA;
}
enum Dessert implements Food {
CUPCAKE, JELLY, CANDY, CHOCOLATE, COOKIES;
}
enum Drink implements Food {
COFFEE, TEA, JUICE, MILK
}
}
这种方式就像是将枚举作为了接口的子类型一样。通过静态导入,就可以使用它:
import enums.menu.Food;
import static enums.menu.Food.*;
public class TypeOfFood {
public static void main(String[] args) {
Food food = Appetizer.SALAD;
food = MainCourse.RICE;
food = Dessert.CANDY;
}
}
通过这种方法,我们就得到了“由接口组织的枚举”,但它还不足以应对所有情况。当我们需要处理一组类型时,接口并没有内置的方法能够为我们提供便利。此时,使用“由枚举组织的枚举”更为管用:
【例子:由枚举来组织枚举】
import onjava.Enums;
public enum Course {
APPETIZER(Food.Appetizer.class),
MAINCOURSE(Food.MainCourse.class),
DESSERT(Food.Dessert.class),
COFFEE(Food.Drink.class);
private Food[] values;
// 接收枚举类型对应的Class对象
private Course(Class<? extends Food> kind) {
values = kind.getEnumConstants();
}
public Food randomSelection() {
return Enums.random(values);
}
}
因为Course是一个枚举类型,因此我们可以直接使用枚举特有的方法:
public class Meal {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
for (Course course : Course.values()) {
Food food = course.randomSelection();
System.out.println(food);
}
System.out.println("======");
}
}
}
程序执行的结果如下:
Course可以直接调用枚举所有的方法,因此可以很方便地进行遍历操作。
上述的做法需要使用接口和枚举。显然,可以将它们整理到一起,形成一种更加清晰的写法:
【例子:在枚举中嵌套枚举】
import onjava.Enums;
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
Set和枚举都对元素的唯一性有所要求,但枚举无法进行增删操作,因此不如Set来得便利。因此,用于配合枚举的Set类型,EnumSet诞生了。
EnumSet的一个目的,是替代原本基于int的“位标记”用法。
这一类型的一大优势就是速度,其内部的实现基于一个long类型的变量(位向量)。
EnumSet中的元素必须来源于某个枚举类型:
【例子:EnumSet的使用】
为了使用EnumSet,首先需要创建一个枚举(报警器的位置信息):
// 报警感应器的位置
public enum AlarmPoints {
STAIR1, STAIR2,
LOBBY,
OFFICE1, OFFICE2, OFFICE3, OFFICE4,
BATHROOM,
UTILITY,
KITCHEN
}
利用这个枚举,下面的程序会展示一些EnumSet的基本用法:
import java.util.EnumSet;
import static enums.AlarmPoints.*;
public class EnumSets {
public static void main(String[] args) {
// 使用noneOf()方法创建一个空的EnumSet
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);
System.out.println();
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);
System.out.println();
// complementOf()方法返回points中未包含的枚举集
points = EnumSet.complementOf(points);
System.out.println(points);
}
}
程序执行的结果是:
EnumSet.of()方法具有多个重载
这是处于性能的考量。尽管这些of()方法可以被一个使用了可变参数的方法替代,但那样做的效率会略低于现在的这种做法。
尽管表格上没有出现,但EnumSet中是存在使用可变参数列表的of()方法的。而如果我们只传入一个参数,编译器不会调用这个of()方法,因此也不会产生额外的开销。
通常情况下,EnumSet是基于64位的long构建的。其中,每个枚举实例需要通过1位来表达其的状态。因此,在使用一个long时,单个EnumSet只能支持包含64个元素的枚举类型。但有时,我们的枚举会超过64个元素:
【例子:超过64个元素的EnumSet】
public class BigEnumSet {
enum Big {
A1, A2, A3, A4, A5, A6, A7, A8, A9, A10,
A11, A12, A13, A14, A15, A16, A17, A18, A19, A20,
A21, A22, A23, A24, A25, A26, A27, A28, A29, A30,
A31, A32, A33, A34, A35, A36, A37, A38, A39, A40,
A41, A42, A43, A44, A45, A46, A47, A48, A49, A50,
A51, A52, A53, A54, A55, A56, A57, A58, A59, A60,
A61, A62, A63, A64, A65
}
public static void main(String[] args) {
EnumSet<Big> bigEnumSet = EnumSet.allOf(Big.class);
System.out.println(bigEnumSet);
}
}
程序执行的结果是:
从输出结果可以看出,若元素超过64个,EnumSet会进行额外的处理。这一点也可以从源代码处了解:
EnumMap
除了EnumSet,也存在EnumMap。这一特殊的Map要求所有的键来自于某个枚举类型。EnumMap内部的实现基于数组,因此有着很高的效率。
与普通的Map相比,EnumMap在操作上的特殊之处只在于:当我们调用put()方法时,只能使用枚举中的值。
【例子:EnumMap的使用例】
import java.util.EnumMap;
import java.util.Map;
import static enums.AlarmPoints.*;
// 使用了命令模式:
// 创建一个接口(只包含一个方法),衍生出不同的实现
interface Command {
void action();
}
public class EnumMaps {
public static void main(String[] args) {
EnumMap<AlarmPoints, Command> em =
new EnumMap<>(AlarmPoints.class);
em.put(KITCHEN,
() -> System.out.println("厨房失火"));
em.put(BATHROOM,
() -> System.out.println("水龙头坏了"));
for (Map.Entry<AlarmPoints, Command> e :
em.entrySet()) {
System.out.println(e.getKey() + ": ");
e.getValue().action();
}
try { // 若不存在指定key值
em.get(UTILITY).action();
} catch (Exception e) {
System.out.println("异常:" + e);
}
}
}
程序执行的结果是:
在上述的em中,所有的枚举元素都有其对应的键。并且根据输出结果的异常显示,所有的键对应的值都会被初始化为null。