枚举是软件开发中使用率非常高的类型。这里针对枚举进行一次深入的讨论,希望对您有所帮助。
枚举的使用
Java 中的枚举是一个比较特殊的类型,既具有 class 的特性,又具有自己特殊的特性。定义枚举类型使用 enum 关键字,枚举值一般使用大写字母,如下所示。使用枚举类型的 name() 方法可以获取字符串的名称,使用 ordinal() 方法可以获取枚举值的下标,这里不做赘述。
enum SexOne {
MALE,FEMALE
}
枚举同样可以拥有构造器和变量,但枚举类型的构造器要求必须是 private 类型。这是为了确保枚举的值一定由自己定义,拒绝外界传入。与 class 不同的是,枚举类型的构造器不定义访问权限时,默认为 private。
enum SexTwo {
MALE("man"),
FEMALE("woman");
String alias;
SexTwo(String alias){
this.alias = alias;
}
}
枚举类型自动拥有 values 和 valueOf 方法,values 用于获取枚举所有的值,valueOf 用于根据名称反查枚举值。在后面的实现原理中,我们将看到这两个方法的实现。
枚举的实现原理
枚举类型编译后生成一个 class 并且继承 Enum 类型(敲黑板,划重点,一定要记住)。反编译可以看到 SexTwo 的字节码,对字节码进行还原可以得到如下的 class。
final class SexTwo extends Enum{
public static SexTwo[] values(){
return (SexTwo[])$VALUES.clone();
}
public static SexTwo valueOf(String s){
return (SexTwo)Enum.valueOf(SexTwo, s);
}
private SexTwo(String s, int i, String s1){
super(s, i);
alias = s1;
}
public static final SexTwo MALE;
public static final SexTwo FEMALE;
String alias;
private static final SexTwo $VALUES[];
static {
MALE = new SexTwo("MALE", 0, "man");
FEMALE = new SexTwo("FEMALE", 1, "woman");
$VALUES = (new SexTwo[] {
MALE, FEMALE
});
}
}
SexTwo 编译后变成了一个普通的 class 类型。这里需要注意的是,SexTwo 拥有修饰符 final,因此不能有子类。同时继承了 Enum 类型,因此开发中 SexTwo 也将不能继承任何父类。
SexTwo 生成的构造器是 private 类型,有两个 public static final SexTwo 类型的变量 MALE 和 FEMALE。编译时编译器自动根据枚举值的顺序确定下标,并在创建枚举值时使用。枚举类型定义的值都是 public static final 类型,在静态初始化中进行初始化。这里可以看到 SexTwo 在静态初始化中为变量 MALE、FEMALE 进行赋值,使用的参数就是我们给定的参数 man 和 woman。
自动生成的 values 和 valueOf
Java 编译枚举类型时,自动加上两个静态方法 values 和 valueOf。如果我们定义了签名完全相同的方法会编译报错 “已在枚举中定义了方法 values”。
枚举类型使用私有静态变量 $VALUES 存储所有的值,在静态初始化中赋值,类型为数组,值为所有枚举值,用于 values 和 valueOf 方法。
values 方法的作用是返回所有枚举值,实现很简单,就是 clone 一下 $VALUES 的值。valueOf 方法的作用是根据枚举值的名称返回枚举值,实现方法是调用 Enum.valueOf 方法,后文会在 Enum 类型中介绍这个方法。
Enum 类型
Enum 类型是 Java 中所有枚举类型的父类,并且是抽象类。需要注意的是我们不能直接继承 Enum 类,只有编译器生成的枚举最终的 class 可以继承,直接继承会导致编译器报错 “类无法直接扩展 java.lang.Enum”。
Enum 类型提供了我们常用的 name() 和 ordinal() 方法,其中的 name 和 ordinal 变量都是 final 类型。
private final String name;
public final String name() {
return name;
}
private final int ordinal;
public final int ordinal() {
return ordinal;
}
Enum 拥有一个构造器,参数为 name 和 ordinal。经过前面的分析可以知道,这个构造器我们是没有办法直接调用的,只有编译器编译的枚举类型可以调用。
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
Enum 定义了一个 valueOf 方法,用于枚举类型调用。可以看到 SexTwo 中的 valueOf 方法就是调用 Enum.valueOf(SexTwo,s) 实现的。
public static > T valueOf(Class 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);
}
Enum 中的大部分方法都是 final 类型的,因此枚举类型是没有办法覆盖这些方法的,唯一能够覆盖的就是 toString 方法。
枚举 与 Class
前面我们已经看到,values 方法是编译器自动加到枚举类型上的,而 Enum 类型并没有提供 values 方法,也就是说我们把枚举类型向上转型为 Enum 类型后没法调用 values 方法。或者我们可能需要传递 Class 对象进行反射操作,这种情况下如何操作枚举呢?
Class 对象的 isEnum() 方法可以用来判断是否是枚举类型,如果是枚举类型该方法返回 true。Class 对象的 getEnumConstants() 方法可以用来获取所有枚举值。
Enum e = SexTwo.MALE;
Class clazz = e.getDeclaringClass();
if(clazz.isEnum()){
SexTwo[] constants = (SexTwo[]) clazz.getEnumConstants();
}
枚举使用抽象方法
前面提到了枚举类型编译之后是以 class 方式运行的,因此枚举类型也可以定义抽象方法。有同学可能要问了,前面说是 final class 类型,怎么可以定义抽象方法呢,抽象方法需要类型是 abstract class ?我们来试一下。
如下,SexThree 定义了一个抽象方法 getAlias()。枚举值 MALE 实现了这个抽象方法,返回 "man"。在枚举中定义抽象方法,可以为多个枚举值定义不同的实现,枚举值自身即可处理数据。
enum SexThree {
MALE {
String getAlias() {
return "man";
}
};
abstract String getAlias();
}
对 SexThree 进行编译和反编译,可以看到,这里生成的类型是抽象类,仍然继承了 Enum。MALE 在编译后生成了 SexThree$1.class,继承 SexThree 并实现了抽象方法。虽然这里枚举类型是抽象类,但是 java 编译器限制了我们不能继承这个抽象类,继承情况下编译时会报错“枚举类型不可继承”。
abstract class SexThree extends Enum {
public static SexThree[] values() {
return (SexThree[])$VALUES.clone();
}
public static SexThree valueOf(String s) {
return (SexThree)Enum.valueOf(test/SexThree, s);
}
private SexThree(String s, int i) {
super(s, i);
}
abstract String getAlias();
public static final SexThree MALE;
private static final SexThree $VALUES[];
static {
MALE = new SexThree("MALE", 0) {
String getAlias()
{
return "man";
}
};
$VALUES = (new SexThree[] {
MALE
});
}
}
枚举实现接口
枚举最终编译成 class,因此也可以实现接口。如下的 SexFour 就实现了 Runnable 接口,定义了 run 方法。因为枚举不能继承其他类型,这个特性使得我们可以通过实现多个方法来定义枚举的某些特征。
enum SexFour implements Runnable {
;
public void run() {
//something
}
}
对 SexFour 进行编译和反编译,可以看到,这里又生成了 final class 类型,并且继承了 Enum,实现了 Runnable 接口。
final class SexFour extends Enum implements Runnable{
public static SexFour[] values(){
return (SexFour[])$VALUES.clone();
}
public static SexFour valueOf(String s){
return (SexFour)Enum.valueOf(test/SexFour, s);
}
private SexFour(String s, int i){
super(s, i);
}
public void run(){
}
private static final SexFour $VALUES[] = new SexFour[0];
}
EnumMap
EnumMap 是一个使用枚举值做 key 的 Map 实现。如下代码即可创建 EnumMap 的实例,创建时需要指定 key 类型的 class,或者传入一个 map 进行初始化。EnumMap 使用数组存储数据,效率高于 HashMap。
//创建一个具有指定键类型的空枚举映射
EnumMap map1=new EnumMap(SexThree.class);
//从一个 EnumMap 创建
EnumMap map2 = new EnumMap(map1);
//从一个 Map 创建
Map map3 = new HashMap<>();
EnumMap map4 = new EnumMap(map3);
EnumMap 使用枚举值做 key,因此选择了枚举值的下标作为值的下标,把值存储在一个数组中。这样显著提高了数据存取效率,但是容量不能发生变化,适合于数据量比较固定又需要较高效率的场景。
public V get(Object key) {
return (isValidKey(key) ?
unmaskNull(vals[((Enum>)key).ordinal()]) : null);
}
public V put(K key, V value) {
typeCheck(key);
int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}
EnumSet
EnumSet 是一个枚举集合,是一个抽象类,它有两个继承类:JumboEnumSet和 RegularEnumSet。EnumSet 使用 bit 位存储数据,效率高于 HashSet。
//创建一个包含指定元素类型的所有元素的枚举 set
EnumSet setAll = EnumSet.allOf(SexTwo.class);
//创建一个指定范围的Set
EnumSet setRange = EnumSet.range(SexTwo.MALE,SexTwo.FEMALE);
//创建一个指定枚举类型的空set
EnumSet setEmpty = EnumSet.noneOf(SexTwo.class);
//复制一个set
EnumSet setNew = EnumSet.copyOf(setRange);
从 JumboEnumSet 的 add 方法可以一窥 EnumSet 的实现原理。首先把枚举值的下标值无符号右移 6 位,也就是按照 64 位进行分组,找到当前分组的数值。然后,按照枚举值的下标值进行左移,找到添加的位置,把该位置置为 1。同样地,EnumSet 的容量也不能发生变化,枚举类型的定义决定了 EnumSet 的固定容量值。
public boolean add(E e) {
typeCheck(e);
int eOrdinal = e.ordinal();
int eWordNum = eOrdinal >>> 6;
long oldElements = elements[eWordNum];
elements[eWordNum] |= (1L << eOrdinal);
boolean result = (elements[eWordNum] != oldElements);
if (result)
size++;
return result;
}
关于枚举就讨论到这里,欢迎留言讨论。
推荐阅读
【Java技术】盘点 Java 中的队列
MyBatis 类型处理器 TypeHandler
【框架探秘】Spring 专题 01. IoC 容器及其原理
MyBatis 动态 SQL 常用功能
Java 9 平台级模块系统
Java 9 新增的 3 个语言新特性
分享学习笔记和技术总结,内容涉及 Java 技术、软件架构、前沿技术、开源框架、数据结构与算法、编程感悟等多个领域,欢迎关注微信公众号“后端开发那点事儿”。