Java enum 枚举类的编译实现

普通类是class,枚举是enum,看起来完全不是一种东西,而且写起来也差别很大,那么枚举到底是什么?编译完之后的枚举又是什么样子的?

这就是本文探讨的内容。

考虑到不同编译器有时候差别很大,这次使用了两种常见的编译器:

javac

$ javac -version
javac 1.7.0_80

ECJ(Eclipse Compiler for Java)

$ java -jar /d/eclipse/plugins/org.eclipse.jdt.core_3.11.2.v20160128-0629.jar -version
Eclipse Compiler for Java(TM) v20160128-0629, 3.11.2, Copyright IBM Corp 2000, 2015. All rights reserved.

枚举对象

就以jls中使用的Color枚举为例:

/* Color.java */
public enum Color {

    RED, GREEN, BLUE;

}

就这段简单的代码,编译之后会变成什么样子呢?

我们需要阅读编译完之后的Color.class文件,但是很多反编译器不会输出这个文件的全部内容,而是为了尽量与源码吻合,删去了编译时添加的内容。因此使用javap -v -p Color命令反汇编得到字节码,阅读字节码即可还原出如下内容:

public final class Color extends Enum<Color> {

    public static final Color RED;
    public static final Color GREEN;
    public static final Color BLUE;

    // ---------- javac ----------
    private static final Color[] $VALUES;

    public static Color[] values() {
        return $VALUES.clone();
    }
    // ---------------------------

    // ---------- ECJ ----------
    // private static final Color[] ENUM$VALUES;
    //
    // public static Color[] values() {
    //     Color[] copy = new Color[ENUM$VALUES.length];
    //     System.arraycopy(ENUM$VALUES, 0, copy, 0, ENUM$VALUES.length);
    //     return copy;
    // }
    // -------------------------

    public static Color valueOf(String name) {
        return (Color) Enum.valueOf(Color.class, name);
    }
    
    private Color(String name, int ordinal) {
        super(name, ordinal);
    }
    
    static {
        RED = new Color("RED", 0);
        GREEN = new Color("GREEN", 0);
        BLUE = new Color("BLUE", 0);
        $VALUES = new Color[3];
        $VALUES[0] = RED;
        $VALUES[1] = GREEN;
        $VALUES[2] = BLUE;
    }

}

大家需要注意的是,这段Java代码是无法编译通过的。

通过上面的代码我们可以得出一些结论:

  1. 代码中声明的enum,在编译时会变成public final class,并且自动继承了Enum<E>。这个动作必须由编译器来完成,直接这样写编译报错。
  2. 枚举中声明的所有枚举对象,编译时都会变成public static final的常量。
  3. 编译时会自动生成private的构造函数,接收两个参数,一个是枚举对象的名字,一个是位置。在构造函数中直接调用了super(String, int),即java.lang.Enumprotected构造函数来构造对象。
  4. 编译时会自动添加static代码块,来初始化所有的枚举对象,并添加到自动生成的一个数组常量中存储起来。这个数组常量的名字不是固定的,完全取决于编译器,ECJ编译的是ENUM$VALUESjavac编译的是$VALUES
  5. 编译时会自动生成public static方法values(),这个是大家很常用的方法,返回所有枚举对象的数组,原来也是在编译时期隐式添加的。两种编译器对此方法的实现稍有不同。
  6. 编译时会自动生成public static方法valueOf(String),直接调用了Enum.valueOf

values数组

我们看到,javac编译出的类有个$VALUES,ECJ编译出的类有个ENUM$VALUES,这个名字有什么特别之处吗?

首先,我们增加一个枚举值,名字就叫$VALUES,然后用javac编译,看看会发生什么:

public enum Color {

    RED, GREEN, BLUE, $VALUES;

}

javac编译之后大概是这个样子:

  public static final Color RED;
  public static final Color GREEN;
  public static final Color BLUE;
  public static final Color $VALUES;
  private static final Color[] $VALUES$;

看来编译器发现$VALUES被占用之后,就改成创建$VALUES$了。接下来再试试,我们把$VALUES$也占用了,果然,编译器创建了一个$VALUES$$

同样的套路,接下来试一试ECJ。在三种颜色枚举值后面加一个名字叫ENUM$VALUES的枚举值,发现编译器创建了一个ENUM$VALUES_0;再加一个ENUM$VALUES_0,编译器就创建一个ENUM$VALUES_1。。

调戏编译器虽然好玩,但是我们也能发现一个结论,就是这个变量的命名是随意的,没有规范的。想反射的同学就需要注意了,通过变量名来反射读取这个数组看来是不太靠谱。

构造函数

上面我们得知编译时会自动生成枚举对象的构造函数,但是在枚举类中自定义构造函数也是很常见的操作,那么会不会发生冲突?到底发生了什么事情?

我们改一下代码,变成下面的样子:

public enum Color {

    RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);

    public int code;

    Color(int code) {
        this.code = code;
        System.out.println("Color " + this + " initialized, code = " + code);
    }

}

同样编译、反汇编之后,翻译得到的构造函数变成了这样:

    private Color(String name, int ordinal, int code) {
        super(name, ordinal);
        this.code = code;
        System.out.println("Color " + this + " initialized, code = " + code);
    }

原来,如果有构造函数的话,会在你的构造函数上强制添加两个参数,而这两个参数是调用super()必需的。
我还验证了一下,如果有多个构造函数,每个构造函数都会被添加参数。

static代码块

我们注意到,编译时会自动添加一个static {},来完成所有的枚举对象的构造和初始化。这和普通的类很不一样,因为普通的类是先初始化类,再初始化对象的。那么,我们需要清晰地掌握枚举类的初始化过程,以免出错。

我们来看下面的代码。

注意代码中,有个static {}来打印RED,如果打印的时候已经构造好了RED对象,则打印结果为RED;如果还没有构造RED对象,则会打印出null
这两种结果截然不同,我们来分析一下:

public enum Color {

    RED, GREEN, BLUE;

    static final String STR = "STR";
    static String str = "str";
    static {
        System.out.println(RED);
    }

    Color() {
        System.out.println(STR);
     // 编译报错:Cannot refer to the static enum field Color.str within an initializer
     // System.out.println(str);
    }

}

同样编译、反汇编之后,翻译得到的static代码块变成了这样:

    static {
        RED = new Color("RED", 0);
        GREEN = new Color("GREEN", 0);
        BLUE = new Color("BLUE", 0);
        $VALUES = new Color[3];
        $VALUES[0] = RED;
        $VALUES[1] = GREEN;
        $VALUES[2] = BLUE;
        str = "str";
        System.out.println(RED);
    }

我们看到,枚举对象的构造和赋值先于所有的static代码。

我们可以得出结论:

枚举类的初始化和枚举对象的初始化过程和普通的类完全不同。普通的类是先完成类的初始化,然后才构造对象,构造对象的时候所有的静态成员都已经准备好了;但是枚举不同,枚举类初始化的第一步就是要构造所有的枚举对象,也就是说,枚举对象构造时,静态成员还完全没有准备好,这也是为什么在枚举对象的构造函数中,引用非finalstatic成员会编译不通过。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值