深刻理解Java枚举

深刻理解Java枚举

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

初看虽然不少,可是共性很大,实际的那些操做码并非不少,多记几遍就能够了

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

尽收眼底的感受真爽!

相关文章
相关标签/搜索