Java枚举

优雅的可选项定义

背景


JDK1.5前没有枚举时,只能简单常量定义

Sample
1
2
3
public static final int HIGH = 0;
public static final int MEDIUN = 1;
public static final int LOW = 2;

常量只是一个值,缺乏组织性,也无从检查合法性,比如方法传入未定义的3
枚举引入了类型系统,直接限定了范围。从基本值变为对象,提供了更优雅更强大的功能

Sample
1
2
3
public enum Level{
HIGH, MEDIUM, LOW
}

使用事项

  • 枚举非常适合作为参数,限定了哪些值合法,即使扩展变动后,新值由于还没被使用,不会造成影响
  • 枚举不适合作为返回值,因为扩展变动后,下游不会知道,如果返回了新值可能出现异常

枚举的本质


枚举类型隐式继承了java.lang.Enum,所以不能再继承其他类
内部有两个变量:声明时的名字和声明时位置,两者都是final不可以重写的,行为永远是返回声明时的值
toString虽然默认也返回名字,但是可以被重写提供更加用户友好的名字

Enum.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
//枚举项声明时名字
private final String name;
public final String name() {
return name;
}

//枚举项声明时的序数
private final int ordinal;
public final int ordinal() {
return ordinal;
}

public String toString() {
return name;
}
//...
}

泛型嵌套Enum<E extends Enum<E>>则枚举Level隐式继承了Enum,即应当为

Sample
1
2
3
public class Level extends Enum<Level> {
//...
}

同时泛型参数要求必须为枚举,添加约束E extends Enum<E>

单例


枚举变量是单例,并且是线程安全

  • 枚举变量在编译后可以观察到内部都是static final的实例,类加载时创建
  • 反序列化是通过内部valueOf,不会新建
    枚举实现了Serializable接口,但是有特殊的一套机制
    序列化时只写名字,反序列化时禁止自定义行为,而是通过valueOf来根据名字查找
    如果服务端提供的接口返回枚举类型,如果服务端增加新变量,老客户端会报错无法找到,有一定兼容性问题
Enum.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException("No enum constant " + enumType.getCanonicalName() + "." + name);
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}

private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}
  • 不支持clone
Enum.java
1
2
3
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
  • 判定相等equals直接比较引用,因此枚举可以直接==比较
Enum.java
1
2
3
public final boolean equals(Object other) {
return this==other;
}

操作方法


除了父类Enum中的方法,编译器自动在枚举上插入静态方法,values()和valueOf()

Sample
1
2
System.out.println(Arrays.toString(Level.values()));  //[HIGH, MEDIUM, LOW]
System.out.println(Level.HIGH == Level.valueOf("HIGH")); //true

比较


枚举项之间比较序数

Enum.java
1
2
3
4
5
6
7
8
public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}

维护额外成员


最常规的枚举只有声明名字和序号,可以给枚举项绑定其他成员变量以及提供方法
声明其他成员后,枚举项末尾要带分号
注意枚举类中不能定义public构造函数,因为不能在外部通过new创建枚举变量,所有变量只能在内部声明,构造方法默认是private

Sample
1
2
3
4
5
6
7
8
9
10
11
12
13
enum Level{
HIGH(1),MEDIUM(2),LOW(3);

private int val;

Level(int val) {
this.val = val;
}

public int getVal() {
return val;
}
}

接口/抽象方法


枚举虽然不能再继承其他类,但是可以实现接口,每个成员都是匿名实现类可以自定义,非常适合用来实现策略模式

Sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface Evaluable{
boolean judge(int val);
}

public enum Level implements Evaluable{
HIGH {
@Override
public boolean judge(int val) {
return val >= 100;
}
},
MEDIUM {
@Override
public boolean judge(int val) {
return val >= 10 && val < 100;
}
},
LOW {
@Override
public boolean judge(int val) {
return val < 10;
}
};
}

抽象方法也可以,原理类似

Sample
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum Level implements Evaluable{
HIGH {
@Override
public boolean judge(int val) {
return val >= 100;
}
},
MEDIUM {
@Override
public boolean judge(int val) {
return val >= 10 && val < 100;
}
},
LOW {
@Override
public boolean judge(int val) {
return val < 10;
}
};

public abstract boolean judge(int val);
}

通过枚举项获取枚举类名,正因为枚举项自身可以有匿名实现体,直接getClass可能不能正确获取,因此需要考虑父类

Enum.java
1
2
3
4
5
6
@SuppressWarnings("unchecked")
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}

总是应当使用getDeclaringClass确保正确

Sample
1
2
System.out.println(Level.HIGH.getClass());  //class Level$1
System.out.println(Level.HIGH.getDeclaringClass()); //class Level

枚举相关集合


EnumMap由枚举类型作为key建立的map, 枚举数量有限,底层为固定大小数组存储,相比通用map更高效

1
2
3
4
EnumMap<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
map.put(DayOfWeek.MONDAY, "1");
map.put(DayOfWeek.FRIDAY, "5");
System.out.println(map); //{MONDAY=1, FRIDAY=5}

EnumSet有部分枚举成员构成集合,可以进行集合操作

1
2
3
EnumSet<DayOfWeek> nonWorkDays = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);
EnumSet<DayOfWeek> workDays = EnumSet.complementOf(nonWorkDays);
System.out.println(workDays); //[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]