Java 枚举类 浅析

JDK 1.5 开始提供了 enum 关键字,用来定义枚举类。Java 中的枚举类,是一组预定义常量的集合,本质上是一种多例设计。

* 本文系OOP的一次作业,欢迎朋友们批评指正。


以下面的Direction枚举类为例:

enum Direction {
    EAST, SOUTH, WEST, NORTH
}

反编译其字节码可以发现,使用 enum 定义的类继承自 Enum 抽象类;由 final 修饰,所以无法被继承。

final class cn.basic.Direction extends java.lang.Enum<cn.basic.Direction>
  minor version: 0
  major version: 61
  flags: (0x4030) ACC_FINAL, ACC_SUPER, ACC_ENUM
  this_class: #1                          // cn/basic/Direction
  super_class: #26                        // java/lang/Enum

枚举类继承了 Enum 中 name 和 ordinal 属性,以及一些相关方法。 

关于 Enum,可以参见:

Enum (Java SE 17 & JDK 17) (oracle.com)https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Enum.html

类的内部,初始化四个枚举常量为静态对象,并且创建了包含这四个对象的对象数组 $VALUES;构造函数私有,所以无法在类外实例化;重写了 Enum 的 values 和 valueOf 方法。

{
  public static final cn.basic.Direction EAST;
    descriptor: Lcn/basic/Direction;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final cn.basic.Direction SOUTH;
    descriptor: Lcn/basic/Direction;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final cn.basic.Direction WEST;
    descriptor: Lcn/basic/Direction;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final cn.basic.Direction NORTH;
    descriptor: Lcn/basic/Direction;
    flags: (0x4019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static cn.basic.Direction[] values();
    descriptor: ()[Lcn/basic/Direction;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #16                 // Field $VALUES:[Lcn/basic/Direction;
         3: invokevirtual #20                 // Method "[Lcn/basic/Direction;".clone:()Ljava/lang/Object;
         6: checkcast     #21                 // class "[Lcn/basic/Direction;"
         9: areturn
      LineNumberTable:
        line 31: 0

  public static cn.basic.Direction valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)Lcn/basic/Direction;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #1                  // class cn/basic/Direction
         2: aload_0
         3: invokestatic  #25                 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #1                  // class cn/basic/Direction
         9: areturn
      LineNumberTable:
        line 31: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  name   Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #1                  // class cn/basic/Direction
         3: dup
         4: ldc           #35                 // String EAST
         6: iconst_0
         7: invokespecial #36                 // Method "<init>":(Ljava/lang/String;I)V
        10: putstatic     #3                  // Field EAST:Lcn/basic/Direction;
        13: new           #1                  // class cn/basic/Direction
        16: dup
        17: ldc           #37                 // String SOUTH
        19: iconst_1
        20: invokespecial #36                 // Method "<init>":(Ljava/lang/String;I)V
        23: putstatic     #7                  // Field SOUTH:Lcn/basic/Direction;
        26: new           #1                  // class cn/basic/Direction
        29: dup
        30: ldc           #38                 // String WEST
        32: iconst_2
        33: invokespecial #36                 // Method "<init>":(Ljava/lang/String;I)V
        36: putstatic     #10                 // Field WEST:Lcn/basic/Direction;
        39: new           #1                  // class cn/basic/Direction
        42: dup
        43: ldc           #39                 // String NORTH
        45: iconst_3
        46: invokespecial #36                 // Method "<init>":(Ljava/lang/String;I)V
        49: putstatic     #13                 // Field NORTH:Lcn/basic/Direction;
        52: invokestatic  #40                 // Method $values:()[Lcn/basic/Direction;
        55: putstatic     #16                 // Field $VALUES:[Lcn/basic/Direction;
        58: return
      LineNumberTable:
        line 32: 0
        line 31: 52
}

具体实现上,在类初始化 clinit 中,就是new了四个方向的对象,分别以其名字和0、1、2、3的序号为参数调用其(实际上是父类 Enum 的)构造函数,然后放到了对应的静态成员中。之后把它们放到了对象数组中。


下面,我编写了一个类,来模拟实现枚举类的一些功能。

final class Direction {
    private final String name;
    private final int ordinal;

    public static final Direction EAST = new Direction("EAST", 0);
    public static final Direction SOUTH = new Direction("SOUTH", 1);
    public static final Direction WEST = new Direction("WEST", 2);
    public static final Direction NORTH = new Direction("NORTH", 3);

    private static final Direction[] $VALUES = new Direction[]{EAST, SOUTH, WEST, NORTH};

    private Direction(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    public final String name() {
        return name;
    }

    public final int ordinal() {
        return ordinal;
    }

    @Override
    public final String toString() {
        return name;
    }

    public static Direction[] values() {
        return $VALUES.clone();
    }

//    public static Direction valueOf(String str) {
//    }

    public final int compareTo(Direction d) {
        return this.ordinal - d.ordinal;
    }
}

注:未实现 valueOf 方法。

测试其功能如下:



然而,正如注释掉的那四行代码所揭示的,自定义类无法像原本的 enum Direction 一样自如地在 switch case 中做判断,因为编译器根本无法解析符号 'EAST' 和 'WEST'。

那么,Java 是如何实现 switch 语句中枚举项的判断的?

我们通过如下测试类探究一二:

public class Demo06 {
    public static void main(String[] args) {
        Direction dir = Direction.EAST;

        /*if (dir == Direction.EAST) {
            System.out.println("东");
        }

        System.out.println(dir.name());
        System.out.println(dir.ordinal());

        for (Direction direction : Direction.values()) {
            System.out.println(direction);
        }

        System.out.println(dir.compareTo(Direction.WEST));

        System.out.println(Direction.valueOf("WEST").ordinal());*/

        switch (dir) {
            case EAST -> System.out.println("E");
            case WEST -> System.out.println("W");
        }

    }
}

enum Direction {
    EAST, SOUTH, WEST, NORTH
}

不难看出,对于枚举类的各种方法,底层无非都是 getstatic 获取枚举常量后 invokevirtual 执行方法,或是直接 invokestatic 通过类名调用静态方法。

那 switch 语句呢?

我们惊奇地发现,这里多了一个叫 Demo06$1 的静态匿名内部类。它是编译器自动生成的,找到并反编译其字节码:

class cn.basic.Demo06$1
  minor version: 0
  major version: 61
  flags: (0x1020) ACC_SUPER, ACC_SYNTHETIC
  this_class: #8                          // cn/basic/Demo06$1
  super_class: #26                        // java/lang/Object
  interfaces: 0, fields: 1, methods: 1, attributes: 4
Constant pool:
   #1 = Methodref          #2.#3          // cn/basic/Direction.values:()[Lcn/basic/Direction;
   #2 = Class              #4             // cn/basic/Direction
   #3 = NameAndType        #5:#6          // values:()[Lcn/basic/Direction;
   #4 = Utf8               cn/basic/Direction
   #5 = Utf8               values
   #6 = Utf8               ()[Lcn/basic/Direction;
   #7 = Fieldref           #8.#9          // cn/basic/Demo06$1.$SwitchMap$cn$basic$Direction:[I
   #8 = Class              #10            // cn/basic/Demo06$1
   #9 = NameAndType        #11:#12        // $SwitchMap$cn$basic$Direction:[I
  #10 = Utf8               cn/basic/Demo06$1
  #11 = Utf8               $SwitchMap$cn$basic$Direction
  #12 = Utf8               [I
  #13 = Fieldref           #2.#14         // cn/basic/Direction.EAST:Lcn/basic/Direction;
  #14 = NameAndType        #15:#16        // EAST:Lcn/basic/Direction;
  #15 = Utf8               EAST
  #16 = Utf8               Lcn/basic/Direction;
  #17 = Methodref          #2.#18         // cn/basic/Direction.ordinal:()I
  #18 = NameAndType        #19:#20        // ordinal:()I
  #19 = Utf8               ordinal
  #20 = Utf8               ()I
  #21 = Class              #22            // java/lang/NoSuchFieldError
  #22 = Utf8               java/lang/NoSuchFieldError
  #23 = Fieldref           #2.#24         // cn/basic/Direction.WEST:Lcn/basic/Direction;
  #24 = NameAndType        #25:#16        // WEST:Lcn/basic/Direction;
  #25 = Utf8               WEST
  #26 = Class              #27            // java/lang/Object
  #27 = Utf8               java/lang/Object
  #28 = Utf8               <clinit>
  #29 = Utf8               ()V
  #30 = Utf8               Code
  #31 = Utf8               LineNumberTable
  #32 = Utf8               LocalVariableTable
  #33 = Utf8               StackMapTable
  #34 = Utf8               SourceFile
  #35 = Utf8               Demo06.java
  #36 = Utf8               EnclosingMethod
  #37 = Class              #38            // cn/basic/Demo06
  #38 = Utf8               cn/basic/Demo06
  #39 = Utf8               NestHost
  #40 = Utf8               InnerClasses
{
  static final int[] $SwitchMap$cn$basic$Direction;
    descriptor: [I
    flags: (0x1018) ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=3, locals=1, args_size=0
         0: invokestatic  #1                  // Method cn/basic/Direction.values:()[Lcn/basic/Direction;
         3: arraylength
         4: newarray       int
         6: putstatic     #7                  // Field $SwitchMap$cn$basic$Direction:[I
         9: getstatic     #7                  // Field $SwitchMap$cn$basic$Direction:[I
        12: getstatic     #13                 // Field cn/basic/Direction.EAST:Lcn/basic/Direction;
        15: invokevirtual #17                 // Method cn/basic/Direction.ordinal:()I
        18: iconst_1
        19: iastore
        20: goto          24
        23: astore_0
        24: getstatic     #7                  // Field $SwitchMap$cn$basic$Direction:[I
        27: getstatic     #23                 // Field cn/basic/Direction.WEST:Lcn/basic/Direction;
        30: invokevirtual #17                 // Method cn/basic/Direction.ordinal:()I
        33: iconst_2
        34: iastore
        35: goto          39
        38: astore_0
        39: return
      Exception table:
         from    to  target type
             9    20    23   Class java/lang/NoSuchFieldError
            24    35    38   Class java/lang/NoSuchFieldError
      LineNumberTable:
        line 22: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
      StackMapTable: number_of_entries = 4
        frame_type = 87 /* same_locals_1_stack_item */
          stack = [ class java/lang/NoSuchFieldError ]
        frame_type = 0 /* same */
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/NoSuchFieldError ]
        frame_type = 0 /* same */
}
SourceFile: "Demo06.java"
EnclosingMethod: #37.#0                 // cn.basic.Demo06
NestHost: class cn/basic/Demo06
InnerClasses:
  static #8;                              // class cn/basic/Demo06$1

一览无余,在合成的内部类中有一个静态成员 int 数组 $SwitchMap$cn$basic$Direction,其长度为枚举常量的个数,然后以枚举常量的 ordinal 为索引为数组元素赋值1、2、3……。这实际上实现了一个个枚举常量到整数的映射。

也就是说,对 Direction dir 的 switch 实际上转换成了对 Demo06$1.$SwitchMap$cn$basic$Direction[dir.ordinal()] 的判断。在 main 函数中,Java 就反向操作,获取要判断的枚举常量的 ordinal,对应到数组中的元素,之后再 lookupswitch 就行得通了。

在字节码层面,可以认为 switch 只支持 int 类型。而 Java 通过这种迂回的方式,扩展到了对枚举类的支持。That explains it.

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值