从新认识Java枚举java
老实说,挺羞愧的,这么久了,一直不知道Java枚举的本质是啥,虽然也在用,可是真不知道它的底层是个啥样的数组
直到2020年4月28日的晚上20点左右,我才真的揭开了Java枚举的面纱,看到了它的真面目,可是我哭了安全
在几个月之前,遇到须要自定义一个mybatis
枚举类型的TypeHandler
,当时有多个枚举类型,想写一个Handler
搞定的,实践中发现,这些枚举类型得有一个共同的父类,才能实现,缺父类?没问题,给它们安排上!mybatis
建立好父类,让小崽子们来认父?app
然而,我觉得小崽子没有爸爸的,谁知道编译器告诉我,它已经有了爸爸!!!框架
那就是java.lang.Enum
这个类,它是一个抽象类,其Java Doc明确写到this
This is the common base class of all Java language enumeration types.
当时也没在乎,有就有了,有了还得我麻烦了。线程
前两天群里有我的问,说重写了枚举类的toString
方法,怎么没有生效呢?设计
先是怀疑他哪里没搞对,不可能重写toString
不起做用的。代理
个人第一动做是进行自洽解释,从结果去推导缘由
这是大忌,代码的事情,就让代码来讲
给出了一个十分好笑的解释
枚举类里的枚举常量是继承自java.lang.Enum,而你重写的是枚举类的toString(),是java.lang.Object的toString()被重写了,因此不起做用
还别说,我当时还挺高兴的,发现一个知识盲点
,打算写下来,如今想来,那不是盲点,是瞎了
不过虽然想把上面的知识盲点
写下来,可是仍是有些好奇,想弄明白怎么回事
由于当时讨论的时候,我好像提到过java.lang.Enum
是Java中全部枚举类的父类,当时说到了是在编译器,给它整个爸爸的,因此想看看一个枚举类编译后是什么样的。
这一看不当紧,才知道当时说那话是多么的好笑
废话很少说,上涩图
上图是枚举类Java源代码
下图是上图编译后的Class文件反编译后的
javap -c classFilePath
反编译后的内容可能不少人都看不懂,我也不咋懂,不过咱们主要看前面几行就差很少了。
第一行就是代表父子关系的类继承,这里就证明,编译器作了手脚的,强行给enum
修饰的的类安排了一个爸爸
下面几行就有意思了
public static final com.example.demo.enu.DemoEnum ONE; public static final com.example.demo.enu.DemoEnum TWO; public static final com.example.demo.enu.DemoEnum THREE; int num;
而后就很容易想到这个
ONE(1), TWO(2), THREE(3); int num;
是多么多么多么的类似!
能够看到,咱们在Java源码中写的ONE(1)
在编译后的其实是一个DemoEnum
类型的常量
ONE == public static final com.example.demo.enu.DemoEnum ONE
编译器帮咱们作了这个操做
也就是说咱们所写的枚举类,其实能够这么来写,效果等同
public class EqualEnum { public static final EqualEnum ONE = new EqualEnum(1); public static final EqualEnum TWO = new EqualEnum(2); public static final EqualEnum THREE = new EqualEnum(3); int num ; public EqualEnum (int num) { this.num = num; } }
这个普通的的Java类,和咱们上面写的
public enum DemoEnum { ONE(1), TWO(2), THREE(3); int num; DemoEnum (int num) { this.num = num; } }
它们真的同样啊,哇槽!
这个同时也解释了个人一个疑问
为啥我枚举类型,若是想表示别的信息数据时,必定要有相应的成员变量,以及一个对应的构造器?
这个构造器谁来调用呢?
它来调用,这个静态块的内容实际上就是<clinit>
构造器的内容
Tps: 以前分不清类初始化构造器,和实例初始化构造器,能够这么理解
能够理解为classloadInit,类构造器在类加载的过程当中被调用,而 则是初始化一个对象的。
static {}; Code: // 建立一个DemoEnum对象 0: new #4 // class com/example/demo/enu/DemoEnum // 操做数栈顶复制而且入栈 3: dup // 把String ONE 入栈 4: ldc #14 // String ONE // int常量值0入栈 6: iconst_0 7: iconst_1 // 调用实例初始化方法 8: invokespecial #15 // Method "<init>":(Ljava/lang/String;II)V // 对类成员变量ONE赋值 11: putstatic #16 // Field ONE:Lcom/example/demo/enu/DemoEnum; // 下面两个分别是初始化TWO 和THREE的,过程同样 14: new #4 // class com/example/demo/enu/DemoEnum 17: dup 18: ldc #17 // String TWO 20: iconst_1 21: iconst_2 22: invokespecial #15 // Method "<init>":(Ljava/lang/String;II)V 25: putstatic #18 // Field TWO:Lcom/example/demo/enu/DemoEnum; 28: new #4 // class com/example/demo/enu/DemoEnum 31: dup 32: ldc #19 // String THREE 34: iconst_2 35: iconst_3 36: invokespecial #15 // Method "<init>":(Ljava/lang/String;II)V 39: putstatic #20 // Field THREE:Lcom/example/demo/enu/DemoEnum; 42: iconst_3 // 这里是新建一个DemoEnum类型的数组 // 推测是直接在栈顶的 43: anewarray #4 // class com/example/demo/enu/DemoEnum 46: dup 47: iconst_0 // 获取Field ONE, 48: getstatic #16 // Field ONE:Lcom/example/demo/enu/DemoEnum; // 存入数组中 51: aastore 52: dup 53: iconst_1 // 获取 Field TWO 54: getstatic #18 // Field TWO:Lcom/example/demo/enu/DemoEnum; // 存入数组 57: aastore 58: dup 59: iconst_2 // 获取Field THREE 60: getstatic #20 // Field THREE:Lcom/example/demo/enu/DemoEnum; // 存入数组 63: aastore // 栈顶元素 赋值给Field DemoEnum[] $VALUES 64: putstatic #1 // Field $VALUES:[Lcom/example/demo/enu/DemoEnum; 67: return }
这就是为啥须要对应的有参构造器的缘由
到这里仍是存有一些疑问
咱们定义了一个枚举类,确定是须要拿来使用的,尤为是当咱们的枚举类还有一些其余有意义的字段的时候
好比咱们上面的例子ONE(1)
,经过1
这个数值,去得到枚举值 ONE
,这是很常见的一个需求。
方式也很简单
DemoEnum[] vals = DemoEnum.values() for(int i=0; i< vals.length; i++){ if(vals[i].num == 1){ return vals[i]; } }
经过上面就能够找到枚举值ONE
但是找遍了咱们本身写的枚举类DemoEnum
和它的强行安排的父类Enum
,都没有找到静态方法values
若是你细心的看到这里,应该是能明白的
咱们上面经过分析反编译后的字节码,看到两处可疑目标
下面这段在开始的截图有出现
public static com.example.demo.enu.DemoEnum[] values(); Code: // 获取静态域 $VALUES的值 0: getstatic #1 // Field $VALUES:[Lcom/example/demo/enu/DemoEnum; // 调用clone()方法 3: invokevirtual #2 // Method "[Lcom/example/demo/enu/DemoEnum;".clone:()Ljava/lang/Object; // 类型检查 6: checkcast #3 // class "[Lcom/example/demo/enu/DemoEnum;" // 返回clone()后的方法 9: areturn
上面之因此要使用
clone()
,是避免调用values()
,将内部的数组暴露出去,从而有被修改的分险,也存在线程安全问题
后面一处,就是在static{}
块最后那部分
从这两处反编译后的字节码,咱们能很清晰明了的知道这个套路了
编译器本身给咱们强行插入一个静态方法values()
,并且还有一个 T[] $VALUES
数组,不过这个静态域在源码没找到,估计是编译器编译时加进去的
到这里还没完,咱们再来看个有意思的java.lang.Class#getEnumConstantsShared
,在java.lang.Class
中有这么个方法,访问修饰符是default
,包访问级别的
T[] getEnumConstantsShared() { if (enumConstants == null) { if (!isEnum()) return null; try { // 看这里 看这里 看这里 final Method values = getMethod("values"); java.security.AccessController.doPrivileged( new java.security.PrivilegedAction<Void>() { public Void run() { values.setAccessible(true); return null; } }); @SuppressWarnings("unchecked") // 还有这里 这里 这里 T[] temporaryConstants = (T[])values.invoke(null); enumConstants = temporaryConstants; } // These can happen when users concoct enum-like classes // that don't comply with the enum spec. // 这里是一个安全保护,防止本身写了一个相似enum的类,可是没有values方法 catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException ex) { return null; } } return enumConstants; }
咱们的valuesOf
方法,在底层就是调用它来实现的,很遗憾的是,这个valuesOf
方法,仅仅实现了经过枚举类型的name
来查找对应的枚举值。
也就是咱们只能经过变量名 name = "ONE"
这种方式,来查找到DemoEnum.ONE
这个枚举值
之前由于枚举用的少,也就仅仅停留在使用的层面,其实在使用的过程当中,也有不少疑惑产生,可是并无真正像如今这样去深究它的实现。
也许是以前动力不足,也许是对未知的恐惧,也许是其余方面的知识准备还不够。
总之,到如今才算真的理解Java枚举
关于其余方面的知识准备不足
,这个我以为仍是值得说一下的,以前我就写过一次说这个事的,由于有些知识点,它并非孤立的,是网状的,咱们在看某一个点的时候,每每就像在一个蜘蛛网上,可是这个网上太多咱们不知道的东西了,因此就很容易出现去不断的补充和它相关的知识点的状况,这个时候就会很累,并且,你最开始想学的那个知识点,也没怎么搞懂。
我也不知道这种方式对不对,对我来讲,我是这样作的,其实不利于快速吸取知识,可是长久下来,会让本身的广度拓展开来,而且遇到一些新的知识点的时候,能够更容易理解它。
拿此次决定看反编译的字节码这个事,若是放在一个月前,我是不敢的,真的不敢,看不懂,头大,不会有这个想法的。
前段时间想把Java的动态代理搞一搞,不少框架都用了动态代理,不整明白,看源码很糊涂。
所以决定看看,而后找到了梁飞关于在设计Dubbo
时对动态代理的选择的一篇文章,里面贴出了几种动态代理生成的字节码的对比,看不到懂,满脑子问号。
后来决定,了解下字节码吧,把《深刻理解Java虚拟机》
这本书翻出来,翻到最后的附录部分,看了一遍
初看虽然不少,可是共性很大,实际的那些操做码并非不少,多记几遍就能够了
我喜欢这种明了的感受,虽然快感后是索然无味
,不过这也能正向激励去不断的探索未知,而不是由于恐惧而退却!
尽收眼底的感受真爽!