深入理解Java枚举
重新认识Java枚举
老实说,挺羞愧的,这么久了,一直不知道Java枚举的本质是啥,虽然也在用,但是真不知道它的底层是个啥样的
直到2020年4月28日的晚上20点左右,我才真的揭开了Java枚举的面纱,看到了它的真面目,但是我哭了
缘起
在几个月以前,遇到需要自定义一个mybatis
枚举类型的TypeHandler
,当时有多个枚举类型,想写一个Handler
搞定的,实践中发现,这些枚举类型得有一个共同的父类,才能实现,缺父类?没问题,给它们安排上!
创建好父类,让小崽子们来认父?
然而,我以为小崽子没有爸爸的,谁知道编译器告诉我,它已经有了爸爸!!!
那就是java.lang.Enum
这个类,它是一个抽象类,其Java Doc明确写到
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虚拟机》
这本书翻出来,翻到最后的附录部分,看了一遍
初看虽然很多,但是共性很大,实际的那些操作码并不是很多,多记几遍就可以了
我喜欢这种明了的感觉,虽然快感后是索然无味
,不过这也能正向激励去不断的探索未知,而不是因为恐惧而退却!
一览无余的感觉真爽!