Java编程笔记19:枚举
图源:PHP中文网
之前在Java编程笔记2:初始化和清理 - 魔芋红茶’s blog (icexmoon.cn)中简单介绍过枚举,在对反射和泛型等内容学习后,我们可以更深入地学习和理解枚举。
基本操作
下面这个例子说明了一些枚举的基本操作:
package ch19.basic;
import util.Fmt;
enum Color {
RED, BLUE, GREEN, YELLO, BLACK
}
public class Main {
public static void main(String[] args) {
Color comparedColor = Color.GREEN;
for (Color color : Color.values()) {
System.out.print(color + "\t");
System.out.print("#" + color.ordinal() + "\t");
String flag = "";
int compare = color.compareTo(comparedColor);
if (compare < 0) {
flag = "<";
} else if (compare == 0) {
flag = "=";
} else {
flag = ">";
}
Fmt.printf("%s %s %s\t", color, flag, comparedColor);
if (color == comparedColor) {
flag = "=";
} else {
flag = "!=";
}
Fmt.printf("%s %s %s\t", color, flag, comparedColor);
if (color.equals(comparedColor)) {
flag = "=";
} else {
flag = "!=";
}
Fmt.printf("%s %s %s\t", color, flag, comparedColor);
System.out.print(color.name() + "\t");
System.out.print(color.getDeclaringClass());
System.out.println();
}
Color c = Enum.valueOf(Color.class, "RED");
System.out.println(c);
}
}
// RED #0 RED < GREEN RED != GREEN RED != GREEN RED class ch19.basic.Color
// BLUE #1 BLUE < GREEN BLUE != GREEN BLUE != GREEN BLUE class ch19.basic.Color
// GREEN #2 GREEN = GREEN GREEN = GREEN GREEN = GREEN GREEN class ch19.basic.Color
// YELLO #3 YELLO > GREEN YELLO != GREEN YELLO != GREEN YELLO class ch19.basic.Color
// BLACK #4 BLACK > GREEN BLACK != GREEN BLACK != GREEN BLACK class ch19.basic.Color
// RED
枚举类型有一个values
方法,会返回一个所有枚举值组成的数组。可以利用它遍历所有的枚举值。
枚举值会按照定义的循序分配一个整型值,可以用ordinal
方法获取。
枚举类型实现了Comparable
接口,所以可以使用compareTo
方法进行比较,比较结果与枚举对值的整型值的比较结果一致。
此外,可以通过compareTo
比较两个枚举值是否相等,或者用==
比较也是同样的效果。
枚举值的name
方法会返回枚举值的字面量对应的字符串,getDeclaringClass
方法可以获取枚举类型。
最后,可以用Enum.valueOf
方法获取枚举值,其第一个参数是枚举类型的Class
对象,第二个参数是枚举值字面量对应的字符串。
静态导入
通常在使用枚举值的时候必须带上枚举类型,比如Color.RED
这样。实际上枚举值是枚举类型的静态属性,所以可以利用静态导入让使用枚举值的方式更简单:
package ch19.static1;
public enum Color {
RED, GREEN, BLUE
}
package ch19.static1;
import static ch19.static1.Color.*;
public class Main {
public static void main(String[] args) {
System.out.println(RED);
System.out.println(GREEN);
System.out.println(BLUE);
System.out.println(RED.getDeclaringClass());
}
}
// RED
// GREEN
// BLUE
// class ch19.static1.Color
这个示例中枚举类型和Main
是分别定义在一个包中的两个java
文件中的,但其实在同一个文件中也可以静态导入枚举值,但似乎没有太大必要。
Enum
如果你使用javap
查看枚举类型的字节码,就会看到类似下面这样的内容:
❯ javap -cp D:\workspace\java\java-notebook\xyz\icexmoon\java_notes ch19.methods2.Color
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Compiled from "Color.java"
public final class ch19.methods2.Color extends java.lang.Enum<ch19.methods2.Color> {
public static final ch19.methods2.Color RED;
public static final ch19.methods2.Color GREEN;
public static final ch19.methods2.Color BLUE;
public static ch19.methods2.Color[] values();
public static ch19.methods2.Color valueOf(java.lang.String);
static {
};
}
这说明Java中的枚举类型实际上是一个继承自java.lang.Enum
类的子类,并且被声明为final
的,因此枚举类型不能被继承。此外,Enum
还是一个泛型类,所以你可以看到,示例中出现Color extends Enum<Color>
这样的内容,类似的写法在泛型部分我们解释过。
枚举值被以静态属性的方式定义,就像示例中的RED
、GREEN
那样。
最后,相比基类Enum
,枚举类型还多出两个静态方法values
和valueOf
。
当然,这些实现细节对开发者是不可见的,都是由Java编译器实现的,我们只需要编写enum{...}
这样的简单的枚举类型定义,编译器会自动帮我们将其转换为字节码中的这种定义。
了解上边这些内容后,我们可以通过反射的方式进一步进行验证:
package ch19.methods2;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import util.Fmt;
public class Main {
public static void main(String[] args) {
Set<String> subMethods = analysis(Color.class);
Set<String> superMethods = analysis(Enum.class);
subMethods.removeAll(superMethods);
System.out.println(subMethods);
}
private static <T extends Enum> Set<String> analysis(Class<T> cls) {
Fmt.printf("========analysis %s=========\n", cls.getName());
System.out.println("super class:" + cls.getSuperclass().getName());
System.out.println("interfaces:" + Arrays.toString(cls.getInterfaces()));
Set<String> methods = new HashSet<>();
for (Method method : cls.getMethods()) {
methods.add(method.getName());
}
System.out.println("methods:" + methods);
return methods;
}
}
// ========analysis ch19.methods2.Color=========
// super class:java.lang.Enum
// interfaces:[]
// methods:[getClass, wait, valueOf, values, notifyAll, compareTo, describeConstable, notify, getDeclaringClass, hashCode, equals, name, toString, ordinal]
// ========analysis java.lang.Enum=========
// super class:java.lang.Object
// interfaces:[interface java.lang.constant.Constable, interface java.lang.Comparable, interface java.io.Serializable]
// methods:[getClass, wait, valueOf, notifyAll, compareTo, describeConstable, notify, getDeclaringClass, hashCode, equals, name, toString, ordinal]
// [values]
这个示例说明了上边的论点,之所以两个方法名称集合求差集后只有values
没有valueOf
,是因为实际上Enum
也有一个静态方法valueOf
,这点之前已经展示过了,不过它需要两个参数,而枚举类型只需要一个。
添加方法
就像上边说的,enum
类型可以看作是一种特殊的类,除了存在一些限制外,大多数情况下可以当作普通的类来使用。这里边也包含添加普通方法和类方法,甚至包括构造函数:
package ch19.methods;
import java.util.Random;
enum Color {
RED("this is a dack color."), BLUE, YELLOW("this is a light color."), BLACK, WHITE;
private String des;
private static Random rand = new Random();
private Color(String des) {
this.des = des;
}
private Color() {
this("default description.");
}
public String getDes() {
return this.des;
}
public static Color getRandomColor() {
Color[] colors = values();
return colors[rand.nextInt(colors.length)];
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
Color c = Color.getRandomColor();
System.out.print(c + " ");
System.out.println(c.getDes());
}
}
}
// BLACK default description.
// BLUE default description.
// WHITE default description.
// BLUE default description.
// WHITE default description.
// YELLOW this is a light color.
要注意的是,为枚举类添加的构造函数必须是private
的,否则无法通过编译。这样规定是合理的,因为实际上除了在枚举类型定义中添加枚举值,你是没法在其它地方添加枚举值的,自然其构造函数也就没有必要是private
以外的访问权限。
还有要注意的是,在为枚举类型添加了其它属性和方法后,就需要在枚举值后添加一个;
作为结束。
因为枚举值可以看作是一种“常量”,所以枚举类型的普通方法也被称作”特定常量方法“(constant-specific methods)。
覆盖Enum的方法
我们已经知道,枚举类型继承自Enum
,因此,可以覆盖Enum
的方法:
package ch19.methods3;
...
enum Color {
...
@Override
public String toString() {
return Fmt.sprintf("%s(%s)", name(), des);
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 6; i++) {
Color c = Color.getRandomColor();
System.out.println(c);
}
}
}
// YELLOW(this is a light color.)
// RED(this is a dack color.)
// RED(this is a dack color.)
// YELLOW(this is a light color.)
// BLUE(default description.)
// BLACK(default description.)
switch
在Java编程笔记1:基础 - 魔芋红茶’s blog (icexmoon.cn)中我们讨论了Java中switch
语句的局限性,并且在Java编程笔记2:初始化和清理 - 魔芋红茶’s blog (icexmoon.cn)中提到可以在switch
语句中使用枚举类型。
下面是一个使用switch
和枚举编写的红绿灯模拟示例:
package ch19.switch1;
enum TrafficLight {
RED, YELLOW, GREEN
}
public class Main {
public static void main(String[] args) {
TrafficLight tl = TrafficLight.RED;
StringBuilder sb= new StringBuilder();
sb.append(tl);
for (int i = 0; i < 10; i++) {
tl = turn(tl);
sb.append("=>");
sb.append(tl);
}
System.out.println(sb.toString());
}
private static TrafficLight turn(TrafficLight tl) {
switch (tl) {
case RED:
return TrafficLight.GREEN;
case GREEN:
return TrafficLight.YELLOW;
case YELLOW:
return TrafficLight.RED;
default:
return TrafficLight.RED;
}
}
}
// RED=>GREEN=>YELLOW=>RED=>GREEN=>YELLOW=>RED=>GREEN=>YELLOW=>RED=>GREEN
这里的红绿灯转换可以看作是一个简单的状态机,有意思的是,将枚举值用于switch
块中的case
关键字后时,可以省略掉枚举类型,这可能是因为switch
条件中已经包含了一个枚举实例,编译器可以根据该实例确定其所属的枚举类型,所以就没必要在case
后明确所属的枚举类型了。但是这也仅仅限于case
条件语句,case
之后的执行语句块中依然需要写明枚举类型。
values()
在前边已经介绍了编译器是如何实现枚举类型,以及如何给枚举类型添加了一个values()
静态方法。这里再介绍一些相关示例。
因为values()
方法是编译器添加的,而Enum
实际上是没有该方法的,所以如果将一个枚举值向上转型为Enum
类型,就无法调用这个方法。
package ch19.values;
enum Color {
RED, YELLOW, GREEN, BLUE
}
public class Main {
public static void main(String[] args) {
Color[] colors = Color.RED.values();
System.out.println(colors);
Enum e = Color.RED;
// colors = e.values();
}
}
注释部分代码无法通过编译。
实际上这里不应该使用Color.RED.values
这样的代码,实际上这是用对象调用静态方法,是不被推荐的方式。但在这个例子中,e.values()
实际上也是试图在用一个Enum
实例调用静态方法,所以是有意为之。
虽然在这个例子中,句柄已经从Color
变成了Enum
,产生了”细节丢失“,也因此无法调用values
获取全部枚举值,但实际上e
的真实类型依然是Color
,并不会因为句柄的改变而改变(这也正是多态的精髓所在)。所以我们可以通过反射来还原真实类型,并获取相应的枚举值:
package ch19.values2;
import java.util.Arrays;
enum Color {
RED, YELLOW, GREEN, BLUE
}
public class Main {
public static void main(String[] args) {
Color[] colors = Color.RED.values();
System.out.println(Arrays.toString(colors));
Enum e = Color.RED;
// colors = e.values();
Enum<Color>[] colors2 = e.getClass().getEnumConstants();
System.out.println(Arrays.toString(colors2));
}
}
// [RED, YELLOW, GREEN, BLUE]
// [RED, YELLOW, GREEN, BLUE]
Class.getEnumConstants
方法可以获取枚举类型的全部枚举值,类似于values
。但是前提必须是Class
对象是一个枚举类型的Class
对象。如果你对一个非枚举类型的Class
对象调用该方法:
package ch19.values3;
public class Main {
public static void main(String[] args) {
System.out.println(Integer.class.getEnumConstants());
}
}
// null
实现,而非继承
前边说了,枚举类型继承自Enum
类,Java也不支持多继承,因此我们不能让枚举类型继承其它类。但是可以让其实现其它接口:
package ch19.interface1;
import ch15.test2.Generator;
enum TrafficLight implements Generator<TrafficLight> {
RED, GREEN, YELLOW;
@Override
public TrafficLight next() {
switch (this) {
case RED:
return GREEN;
case GREEN:
return YELLOW;
case YELLOW:
return RED;
default:
return RED;
}
}
}
public class Main {
public static void main(String[] args) {
Generator<TrafficLight> gen = TrafficLight.RED;
StringBuilder sb = new StringBuilder();
sb.append(gen);
for (int i = 0; i < 10; i++) {
gen = gen.next();
sb.append("=>");
sb.append(gen);
}
System.out.println(sb.toString());
}
}
// RED=>GREEN=>YELLOW=>RED=>GREEN=>YELLOW=>RED=>GREEN=>YELLOW=>RED=>GREEN
在这个示例中,枚举类型TrafficLight
实现了Generator
接口,可以看作是一个生成器模式的应用,不过因为TrafficLight
也可以看作一个简单的状态机,所以也可以看作是一个状态模式,交通灯可以由next
方法从一个状态转换到下一个状态。
关于状态模式,可以阅读设计模式 with Python 10:状态模式 - 魔芋红茶’s blog (icexmoon.cn)。
随机选取
经常会有从某个枚举类型中随机选一个枚举值的需求,可以为此编写一个工具方法:
package util;
import java.util.