深入理解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

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

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

下面这段在开始的截图有出现

 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虚拟机》这本书翻出来,翻到最后的附录部分,看了一遍

初看虽然很多,但是共性很大,实际的那些操作码并不是很多,多记几遍就可以了

我喜欢这种明了的感觉,虽然快感后是索然无味,不过这也能正向激励去不断的探索未知,而不是因为恐惧而退却!

一览无余的感觉真爽!

深入理解Java枚举的更多相关文章

  1. 深入理解Java枚举类型(enum)

    https://blog.csdn.net/javazejian/article/details/71333103 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(en ...

  2. 如何理解java枚举,看例子

    先来看一下不用枚举怎么表示常量: //常量类 class Num { public static String ONE = "ONE"; public static String ...

  3. 理解Java枚举类型

    (参考资料:深入理解java enum) 1.原理:对编译后的class文件javap反编译可以看出,定义的枚举类继承自java.lang.Enum抽象类且通过public static final定 ...

  4. 夯实Java基础系列14:深入理解Java枚举类

    目录 初探枚举类 枚举类-语法 枚举类的具体使用 使用枚举类的注意事项 枚举类的实现原理 枚举类实战 实战一无参 实战二有一参 实战三有两参 枚举类总结 枚举 API 总结 参考文章 微信公众号 Ja ...

  5. 深入理解 Java 枚举

  6. 全面理解Java内存模型(JMM)及volatile关键字(转载)

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...

  7. 深入理解Java并发之synchronized实现原理

    深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...

  8. 深入理解Java类加载器(ClassLoader) (转)

    转自: http://blog.csdn.net/javazejian/article/details/73413292 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Ja ...

  9. 全面理解Java内存模型(JMM)及volatile关键字(转)

    原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...

随机推荐

  1. WEB缓存系统之varnish状态引擎

    前文我们聊了下varnish的VCL配置以及语法特点,怎样去编译加载varnish的vcl配置,以及命令行管理工具varnishadm怎么去连接varnish管理接口进行管理varnish,回顾请参考 ...

  2. Java 为 Excel 中的行设置交替背景色

    在制作Excel表格时,通过将数据表中上下相邻的两行用不同的背景色填充,可以使各行的数据看起来更清楚,避免看错行,同时也能增加Excel表格的美观度.本文将介绍如何在Java程序中为 Excel 奇数 ...

  3. reuire代码优化之:r.js

    r.js是requireJS的优化(Optimizer)工具,可以实现前端文件的压缩与合并,在requireJS异步按需加载的基础上进一步提供前端优化,减小前端文件大小.减少对服务器的文件请求.要使用 ...

  4. Git-flow 使用笔记

    git-flow 原理:A successful Git branching model,两篇不错的中文翻译: Git开发管理之道,一个成功的Git分支模型. 简单来说,git-flow 就是在 gi ...

  5. Hadoop(八):YARN框架简介

    YARN组件图 Container是YARN框架中对应资源的抽象,封装了运行节点上的资源(内存+CPU) NodeManager负责Container状态的维护,通过心跳,把资源信息(剩余CPU.内存 ...

  6. MyBatis(二):基础CRUD

    本文是按照狂神说的教学视频学习的笔记,强力推荐,教学深入浅出1便就懂!b站搜索狂神说即可 https://space.bilibili.com/95256449?spm_id_from=333.788 ...

  7. Vulnhub DC-6靶机渗透

    信息搜集 nmap -sP 192.168.146.0/24 #找靶机ip nmap -sS -Pn -A 192.168.146.143 #扫描靶机信息 22和80端口,老朋友了. 先直接访问htt ...

  8. PTA 6-1 单链表逆转

    本题是一个非常经典的题目:单链表逆转. 这是链表结点的定义: typedef struct Node *PtrToNode; struct Node { ElementType Data; /* 存储 ...

  9. TP3快速入门

    一.查询 D方法实例化模型类的时候通常是实例化某个具体的模型类,如果你仅仅是对数据表进行基本的CURD操作的话,使用M方法实例化的话,由于不需要加载具体的模型类,所以性能会更高. $map = arr ...

  10. "斜体显示"组件:<i> —— 快应用组件库H-UI

     <import name="i" src="../Common/ui/h-ui/text/c_tag_i"></import> &l ...