深入理解 Java 枚举类型


枚举的使用

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 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);
}

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<SexThree,String> map1=new EnumMap<SexThree,String>(SexThree.class);
//从一个 EnumMap 创建
EnumMap<SexThree,String> map2 = new EnumMap<SexThree,String>(map1);
//从一个 Map 创建
Map<SexThree, ? extends String> map3 = new HashMap<>();
EnumMap<SexThree,String> map4 = new EnumMap<SexThree, String>(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<SexTwo> setAll = EnumSet.allOf(SexTwo.class);
//创建一个指定范围的Set
EnumSet<SexTwo> setRange = EnumSet.range(SexTwo.MALE,SexTwo.FEMALE);
//创建一个指定枚举类型的空set
EnumSet<SexTwo> setEmpty = EnumSet.noneOf(SexTwo.class);
//复制一个set
EnumSet<SexTwo> 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;
}