温故知新-从字节码深入理解枚举

486 阅读4分钟

1. 枚举基础

  • 从jdk 5.0开始,引入了enum关键字,它与class,interface具有同一层级的特性。枚举类的定义由enum替代class关键字实现。
  • 使用enum来定义一个枚举类时,编译器会自动创建一个final类型的类继承于Enum抽象类:public final class T extends Enum,所以,enum类不能再继承于其他类,它已隐式地继承了Enum类;它也不能再被其他类继承,因为已由final修饰。
  • 枚举常量的创建必须放在枚举类中的第一句,其创建方式:常量名(实参列表)
  • 枚举类同普通类一样,可以定义属性、构造方法、普通方法、抽象方法等,只是枚举类允许枚举常量自己对抽象方法的特有实现。
  • 下面是一个四则运算操作符的枚举类型定义,包含了成员变量、构造方法、抽象方法的定义。ADD、SUB等是枚举类的枚举常量,每个枚举常量都实现了calculate抽象方法。
public enum Operator {
    ADD("+") {
        @Override
        public int calculate(int a, int b) {
            return a + b;
        }
    },
    SUB("-") {
        @Override
        public int calculate(int a, int b) {
            return a - b;
        }
    },
    MUL("*") {
        @Override
        public int calculate(int a, int b) {
            return a * b;
        }
    },
    DIV("/") {
        @Override
        public int calculate(int a, int b) {
            return a / b;
        }
    };

    Operator (String operator) {
        this.operator = operator;
    }

    private String operator;

    public abstract int calculate(int a, int b);

    public String getOperator() {
        return operator;
    }
}

2. 枚举原理分析

上面的基础介绍中,不知大家有没有注意到这么一句话:枚举类允许枚举常量自己对抽象方法的特有实现,四则运算操作符的例子中每个枚举常量也都实现了calculate的抽象方法。重写抽象方法,不应该是类层级的动作吗?这里又是怎么做到的?我们利用Operator类的反编译信息来深挖一下。

javac编译Operator类后生成五个字节码文件,除了Operator.class,还生成了Operator$1~4.class四个文件,一般另外生成的文件都是由内部类生成,这与我们在Operator类内部声明的四个枚举值相关联:

枚举-javac Operator类生成的字节码文件

2.1 javap Operator.class

使用javap Operator.class指令反编译枚举类型Operator,得到的信息如下:

枚举-javap Operator.class

从以上的反编译信息中可以得出:

  • 枚举类实际是一个继承了Enum类的抽象类,因此枚举类无法实例化。
  • 具体的枚举值是由static final修饰的Operator类型实例,从生成的字节码文件也不难得出,这四个对象实际上是继承了Operator抽象类的子类Operator$1Operator$2Operator$3Operator$4的实例。
  • 编译出来的Operator类还多出了两个静态方法:valuesvalueOf,还生成了一个静态代码块static{}。
  • 构造方法的参数由一个变成了三个,编译器增加了前面两个参数。

使用javap Operator$1.class指令来反编译Operator$1,得到信息如下:

枚举-javap_Operator$1

Operator$1类是继承于Operator的final类,其内部只包含了构造方法和calculate方法。所以每个枚举常量都能够重写calculate抽象方法,就是通过隐式生成的这四个内部类Operator$1~4来实现的。

我们使用javap -c -v Operator.class查看更详细的反汇编信息,继续分析其中重要的信息点。

2.2 InnerClasses字段

声明了其中包含了四个静态内部类Operator$1~4,定义的每一个枚举值实际都会生成一个内部类,继承于Operator类: 枚举-Operator_InnerClasses

2.3 静态代码块字段

静态代码块字段的具体执行内容分析如下:

枚举-静态代码块

在静态代码块中,会分别调用四个内部类的构造方法创建一个实例对象并分别存储到静态域ADD,SUB,MUL和DIV中,同时会创建一个长度为4类型为Operator的数组,存储这四个内部类对象,并将该数组存储到编译器生成的一个静态字段$VALUES中。

2.4 构造方法多出的两个参数

静态代码块的注释中,创建Operator$1对象传入了三个参数:"ADD", 0, "+",同理,创建其他类对象也都是传入了三个参数,而我们定义的Operator类只有一个参数"+",多出来的前两个参数又有什么作用呢?

在Enum抽象类中中定义了两个final属性:name和ordinal,根据注释,name表示枚举常量中声明的该枚举常量的名称,即我们定义的ADD,SUB等,ordinal表示该枚举常量的序号,该序号根据定义顺序从0开始编号。

public abstract class Enum<E extends Enum<E>> ... {
    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;
    }
}

2.5 编译器生成的values方法

values方法是一个public static方法,该方法会读取在编译器生成,在静态代码块中初始化的VALUES字段,并clone该VALUES字段的值,将类型强转为Operator[]后返回。

通过调用该方法,我们可以返回这个枚举类的枚举数组,由前面分析可知,这个VALUES字段存储的是四个内部类对象数组,即为枚举值数组。 枚举-values

2.6 编译器生成的valueOf方法

同样,valueOf方法也是一个public static方法,该方法用于获取参数String对应的枚举常量,比如Operator.valueOf("ADD");实际会返回Operator$1实例对象。

valueOf方法是通过调用父类Enum.valueOf方法实现的。

Enum.valueOf方法首先通过getSharedConstants方法获取values数组,getSharedConstants方法实际也是调用了编译器生成的values方法,获取到了枚举值数组。通过遍历枚举数组,比较其name属性与传入的参数,最终返回对应的枚举常量。

public abstract class Enum<E extends Enum<E>> ... {
    ...
    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        ...
        T[] values = getSharedConstants(enumType);
        ...
        for (int i = values.length - 1; i >= 0; --i) {
            T value = values[i];
            if (name.equals(value.name())) {
                return value;
            }
        }
        ...
    }
    ...
}

2.7 Enum类

  • Enum类内部的属性以及equalshashCodecompareTo等方法都声明为final,保证了枚举类型的不可变性。
  • 将clone方法声明为final,保证枚举类型不可复制,从而有效阻止单例模式的攻击。
  • readObject方法声明为private,不可被子类重写,从而拦截反序列化的攻击。
public abstract class Enum<E extends Enum<E>> ... {
    ...
    /**
     * Throws CloneNotSupportedException.  This guarantees that enums
     * are never cloned, which is necessary to preserve their "singleton"
     * status.
     *
     * @return (never returns)
     */
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
    ...
    /**
     * prevent default deserialization
     */
    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");
    }
    ...
}

3. 小结

  • 枚举实际上是java的一种语法糖,它本质上还是通过普通类来实现的,只不过许多工作都交由编译器来完成。
  • 枚举值在枚举类中实际是被声明为static final,类型为枚举类的成员变量。
  • 每一个枚举值实际都会对应构造一个继承于枚举类的内部类,这也是枚举值能重写抽象方法的原因。
  • 在类的静态代码块中,每个内部类都会创建一个对象实例,对应于每个枚举值。最终存储到由编译器生成的静态字段VALUES数组中。
  • 编译器会自动生成values和valueOf方法,通过这两个方法可以获取所有的枚举值对象实例,并访问对象的属性值。

参考:blog.csdn.net/mhmyqn/arti…