业务遇到需求,细化三点:信息固定,用枚举记录;不同个体存在先后,枚举类提供公共的“根据特定顺序的个体的list”的方法;部分信息不明,部署时需根据配置文件变更。
借此需求,研究了ENUM的编译加载规则。
涉及的模块按顺序排列如下:
(1)、普通代码块;(2)、构造器;(3)、静态变量/方法;(4)、静态代码块;(5)、普通方法
显然不同于普通类的“先静态代码块、后构造”的流程,原因应该在于“枚举本身就是一种静态常量”,这种“静态”优先于静态方法块的“静态”。同时,实际处理类变量会更复杂些。
5个步骤中,(1)与(2)是真正的对应与“个体”的构造环节,与普通类相比依然是代码块先于构造器被编译,但是这个“个体”更像是“归属于类的变量”,优先级最高;特别的,(1)与(2)构造环节未完成时,枚举类的个体并未被赋值,枚举个体的获取、以及类名XX.values的获取均为null,debug可发现(因此需要将枚举类的实际个体与构造声明相区分,不是执行了调用函数就立即有了对应枚举实际个体)。当然,与普通类相同的是枚举个体声明也只是构造器的调用,而且,代码块也依然是构造器的一部分,“调用一次”就执行一次(区别于静态代码块)。
步骤(3)主要是加载静态变量和方法,方法只是加载进内存,并不执行。自此,后续方法都可以调用之。
步骤(4)执行静态代码块,仅一次。
步骤(5)不必解释。
枚举的对象、方法调用出现问题时,就会出现“枚举类java.lang.ExceptionInInitializerError”的异常报错,且只在初始化的首次报错,后续用到该枚举,只会报“java.lang.NoClassDefFoundError: Could not initialize class xxx”。
借此看下Java类的编译加载原理。
首先,回到ENUM类中,步骤(1)与(2)当然不能调用静态变量静态方法,但是可以调用普通变量普通方法。步骤(2)中可以使用this,获取的就是当前枚举个体(但是前述说了不能调用声明变量与枚举类,因为该实体还没挂钩的对应声明上),对应也就可以读取普通变量、方法,均可调用;而通过调用普通方法间接调用静态变量,则会执行时报错,同时出现虚拟机的clinit、init方法。问题来了
clinit是类级别的构造方法,init是构造特定对象个体的构造方法,而现在出现的是clinit中调用了init方法,还是对应于“枚举的个体本质还是个constant实体”的背景。
clinit are the static initialization blocks for the class, and static field initialization.
init is the (or one of the) constructor(s) for the instance, and non-static field initialization.
则clinit是对类与类级别静态方法的初始化,init是对实体、非静态的初始化。一般而言,肯定是先clinit后init,即先静态后非静态,枚举看似不是,但其本质静态属性使其构造环节依然是个“静态过程”,而且是“clinit调用的init”。
另外,父子类的调用流程是:父静态代码块、子静态代码块、父代码块、父构造器、子代码块、子构造器。可见,“静态优先”的原则依然没变,是抽象类级别,无视了父类的非静态的“构造环节”。