深入理解Java枚举

深入理解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.ObjecttoString()被重写了,所以不起作用

还别说,我当时还挺高兴的,发现一个知识盲点,打算写下来,现在想来,那不是盲点,是瞎了

不过虽然想把上面的知识盲点写下来,但是还是有些好奇,想弄明白怎么回事

因为当时讨论的时候,我好像提到过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

如果你细心的看到这里,应该是能明白的

我们上面通过分析反编译后的字节码,看到两处可疑目标

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpgpyy.html