Java枚举类型原理

版权声明:本文为 冬夏 原创文章,可以随意转载,但请注明出处。

枚举(Enum)类型是 java 5 中新增的一种数据类型,它能够帮助我们更加快捷和安全的实现枚举。

回想之前我们在定义枚举常量时的做法:

public static final int MONDAY =1;
public static final int TUESDAY=2;
public static final int WEDNESDAY=3;
public static final int THURSDAY=4;
public static final int FRIDAY=5;
public static final int SATURDAY=6;
public static final int SUNDAY=7;

这样的定义方式虽然也能正常工作,但却存在许多不足,比如不小心把 MONDAY 和 TUESDAY 都置为 2 时,编译器并不会报错,但是却很难进行排查。

在 Java 5 之后,我们可以用如下的定义定义枚举类型。

public enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY;
}

并可以在程序中通过 Day.MONDAY 的方式使用,这无疑大大提高了程序的安全性。

我们知道,编程语言的设计具有前向兼容性,这意味着后续的语言特性实际上都是通过语法糖来实现的,那么枚举类型的内部实现原理是怎么样的呢?我们通过将上面的枚举类 Day 通过 javap 命令进行反编译,再将反编译的代码通过改写使其更加容易阅读,最终得到下面的反编译代码。

public final class Day extends Enum<Day>
{
    //编译器为我们添加的静态的values()方法
    public static Day[] values()
    {
        return (Day[])$VALUES.clone();
    }
    //编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法
    public static Day valueOf(String s)
    {
        return (Day)Enum.valueOf(Day, s);
    }
    //私有构造函数
    private Day(String s, int i)
    {
        super(s, i);
    }
     //前面定义的7种枚举实例
    public static final Day MONDAY;
    public static final Day TUESDAY;
    public static final Day WEDNESDAY;
    public static final Day THURSDAY;
    public static final Day FRIDAY;
    public static final Day SATURDAY;
    public static final Day SUNDAY;
    private static final Day $VALUES[];

    static
    {    
        //实例化枚举实例
        MONDAY = new Day("MONDAY", 0);
        TUESDAY = new Day("TUESDAY", 1);
        WEDNESDAY = new Day("WEDNESDAY", 2);
        THURSDAY = new Day("THURSDAY", 3);
        FRIDAY = new Day("FRIDAY", 4);
        SATURDAY = new Day("SATURDAY", 5);
        SUNDAY = new Day("SUNDAY", 6);
        $VALUES = new Day[] {
            MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        };
    }
}

通过阅读上面的反编译后代码,我们可以对枚举类型的实现进行总结:

  1. 枚举类型是通过继承 Enum 类来实现的,并且最终生成 final 类来强化不可变性。
  2. 枚举类型的构造函数为私有的,具有 String 和 int 两个参数,分别代表枚举的名称和序号,序号按照定义的顺序从小到大排列。
  3. 每一个枚举都代表一个枚举类事例,并且为 static 和 final,在枚举类的静态代码块中进行初始化,并且有一个 $VALUES 数组保存所有的枚举。
  4. 通过 valueOf 方法可以完成枚举名称到枚举对象的查找。
  5. values()方法返回的是 $VALUES 数组的克隆对象,这能防止$VALUES 数组被篡改。

接下来,我们来分析一下自定义枚举类型所继承的 Enum 类

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

    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    public String toString() {
        return name;
    }

    public final boolean equals(Object other) {
        return this==other;
    }

    public final int hashCode() {
        return super.hashCode();
    }

    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() &&
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }

    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }

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

    protected final void finalize() { }

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

通过阅读上面的 Enum 抽象类,我们可以对其特点进行总结:

  1. 由 equals 方法可知每个枚举只与自身相等,没有等效相等的枚举。
  2. 由 clone 方法可知枚举对象不允许克隆,这能保证每一个枚举都是唯一的。
  3. 由 compareTo 方法可知枚举只能与同类型的枚举相比较,返回结果为枚举的顺序之差。
  4. 由 finalize 方法可知枚举类不允许实现 finalize 方法,这与枚举的安全性有关。
  5. 由 readObject 方法和 readObjectNoData 方法可知枚举对象不允许反序列化,这也能保证每一个枚举都是唯一的。

由以上的分析我们可以发现,枚举的最大特点就是唯一性,同时可以发现只有单个元素的枚举在不经意间符合了单例模式的要求,具体为:

  1. 枚举类为不可变类,这防止了单例类被继承。
  2. 枚举类型的构造函数为私有的,因此不能主动创建对应的单例对象。
  3. 枚举的元素为 public static final 类型,并且在类加载的时候在静态代码块内完成了初始化,这相当于单例模式的恶汉模式。
  4. 枚举类对象不允许克隆,这能保证每一个单例都是唯一的。
  5. 枚举类对象不可反序列化,这也能保证每一个单例都是唯一的。

因此我们可以使用单元素的枚举类型来实现单例模式。事实上,正如 Effective Java 一书中所说的:单元素的枚举类型已经成为实现 Singleton 的最佳方法。

注意 :值得提出的时,由以上的分析我们知道每一个枚举都是一个对象,既然是对象,那么它所占的内存就比基本类型大很多,这就是枚举类型的缺点,所以在Android开发中并不建议使用枚举类型,而是使用@interface+@IntDef/@StringDef等注解加上int或者String进行替代。

冬夏 wechat
欢迎您扫一扫上面的二维码,关注我的个人公众号:Android从入门到精通
坚持原创技术分享,您的支持将鼓励我继续创作