深入理解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. Java中使用RSA算法加密

    Java中使用RSA算法加密 概述 RSA加密算法是一种非对称加密算法 RSA加密的方式 使用公钥加密的数据,利用私钥进行解密 使用私钥加密的数据,利用公钥进行解密 RSA是一对密钥.分别是公钥和私钥 ...

  2. Spring Boot 整合视图层技术,application全局配置文件

    目录 Spring Boot 整合视图层技术 Spring Boot 整合jsp Spring Boot 整合freemarker Spring Boot 整合视图层技术 Spring Boot 整合 ...

  3. 3.Metasploit攻击流程及命令介绍

    Metasploit 进阶第一讲    攻击流程及命令介绍   01.渗透测试过程环节(PTES)   1.前期交互阶段:与客户组织进行交互讨论,确定范围,目标等 2.情报搜集阶段:获取更多目标组织信 ...

  4. python基础知识 目录 简介

    1.1编程语言介绍与分类 什么是编程语言? 本质:与人类语言一样.沟通 电流+一堆硬件 高电压1 低电压0 高电压1 低电压0 高电压1 低电压0 8 晶体管 010101010101 play so ...

  5. B 方块消消乐

    时间限制 : - MS   空间限制 : - KB  评测说明 : 1s,128m 问题描述 何老板在玩一款消消乐游戏,游戏虽然简单,何老板仍旧乐此不疲.游戏一开始有n个边长为1的方块叠成一个高为n的 ...

  6. Controller与RestController的区别

    在使用Spring系列的框架的时候,相信几乎所有人都会遇见@Controller与@RestController两个注解,那么这两个注解到底有什么区别? 1. 标记有@RestController的类 ...

  7. background-clip 和 background-origin 有什么区别? -[CSS] - [属性]

    这两个属性在W3S上的示例,给人的感觉好像效果是一样的:

  8. php连接数据库,php连接mysql并查询的几种方式,PHP PDO连接以及预处理

    PHP连接数据库 面向过程 $config = [ 'host'=>'127.0.0.1', //数据库地址 'name'=>'test', //库名 'user'=>'root', ...

  9. ubuntu 虚拟机复制后打开蓝屏解决办法

    sudo apt-get install xserver-xorg-lts-utopic sudo dpkg-reconfigure xserver-xorg-lts-utopic reboot

  10. Python爬虫系列(一):从零开始,安装环境

    在上一个系列,我们学会使用rabbitmq.本来接着是把公司的celery分享出来,但是定睛一看,celery4.0已经不再支持Windows.公司也逐步放弃了服役多年的celery项目.恰好,公司找 ...