目录
枚举的基本特性
枚举类型中的自定义方法
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;}@Overridepublic 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;}// 也可以进行方法重载:@Overridepublic 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;}}@Overridepublic 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 LetterCharacterimplements Supplier<LetterCharacter> {A, B, C, D, E, F, G;private Random rand =new Random(47);@Overridepublic 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()方法创建一个空的EnumSetEnumSet<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。