[Java] 浅析枚举的实现

171 阅读3分钟

浅析 java 中枚举(enum)的实现

Java1.5 开始支持枚举(enum) 类型,大家应该都已经用过枚举类型了,在 class 文件层面,枚举是如何实现的呢?本文对此进行分析。

要点

image.png

正文

代码及准备工作

代码

我们用一个简单的 enum class 来进行讨论 ⬇️(请将以下代码保存为 Direction.java

public enum Direction {
    EAST("东"),
    WEST("西"),
    SOUTH("南"),
    NORTH("北");

    private final String desc;

    Direction(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

    public static void main(String[] args) {
        for (Direction d : Direction.values()) {
            System.out.printf("For Direction instance: %s, its ordinal is: %s%n", d.name(), d.ordinal());
        }
    }
}

Directionmain(String[]) 方法中,我们调用了 Direction 类中的 3 个方法 ⬇️

  • name()
  • ordinal()
  • values() (⬅️ 它是 Direction 的静态方法)

但是在 Direction.java 中,并没有显式定义这 3 个方法,那么这 3 个方法是从哪里来的呢?我们来进行探索吧。

相关的命令
# ⬇️ 用这个命令可以编译 Direction.java
javac -g -parameters Direction.java

# ⬇️ 用这个命令可以查看 Direction.class 的简要内容
javap -p Direction

# ⬇️ 用这个命令可以查看 Direction.class 的详细内容
javap -v -p Direction

javac -g -parameters Direction.java 命令编译 Direction.java 后,会得到 Direction.class 文件。 用 javap -p Direction 命令可以查看 Direction.class 文件的简要内容。其结果如下 ⬇️

Compiled from "Direction.java"
public final class Direction extends java.lang.Enum<Direction> {
  public static final Direction EAST;
  public static final Direction WEST;
  public static final Direction SOUTH;
  public static final Direction NORTH;
  private final java.lang.String desc;
  private static final Direction[] $VALUES;
  public static Direction[] values();
  public static Direction valueOf(java.lang.String);
  private Direction(java.lang.String);
  public java.lang.String getDesc();
  public static void main(java.lang.String[]);
  private static Direction[] $values();
  static {};
}

可见 Direction extendjava.lang.Enum。 我画了张简单的类图⬇️

image.png

javap -v -p Direction 命令可以查看 Direction.class 文件的详细内容, 但完整的结果有点长,这里就不展示了。基于 javap -v -p Direction 命令给出的结果,我们可以尝试把对应的 java 代码写出来 ⬇️ (其中的注释是我额外添加的)

// 以下代码是我手动转化的,不保证绝对准确,仅供参考
public final class Direction extends java.lang.Enum<Direction> {
  public static final Direction EAST;
  public static final Direction WEST;
  public static final Direction SOUTH;
  public static final Direction NORTH;
  
  private final java.lang.String desc;
  
  // $VALUES 是合成字段,在 java 代码中找不到它,
  // 为了便于理解,我把它也展示出来了
  private static final Direction[] $VALUES;
  
  // 按照 https://docs.oracle.com/javase/specs/jls/se24/html/jls-8.html#jls-8.9.3 的描述,
  // 需要有这个方法 ⬇️
  public static Direction[] values() {
    return (Direction[]) $VALUES.clone();
  }
  
  // 按照 https://docs.oracle.com/javase/specs/jls/se24/html/jls-8.html#jls-8.9.3 的描述,
  // 需要有这个方法 ⬇️
  public static Direction valueOf(java.lang.String name) {
    return (Direction) Enum.valueOf(Direction.class, name);
  }
  
  // $enum$name 和 $enum$ordinal 都是编译器合成的参数,在 java 代码中找不到它,
  // 为了便于理解,我把它们也展示出来了
  private Direction(java.lang.String $enum$name, int $enum$ordinal, java.lang.String desc) {
    super($enum$name, $enum$ordinal);
    this.desc = desc;
  }
  
  public java.lang.String getDesc() {
    return this.desc;
  }
  
  public static void main(java.lang.String[]) {
    // 略
  }
  
  // $values 是合成方法,在 java 代码中找不到它,
  // 为了便于理解,我把它也展示出来了
  private static Direction[] $values() {
    return new Direction[] {EAST, WEST, SOUTH, NORTH};
  }
  
  static {
    EAST = new Direction("EAST", 0, "东");
    WEST = new Direction("WEST", 1, "西");
    SOUTH = new Direction("SOUTH", 2, "南");
    NORTH = new Direction("NORTH", 3, "北");
      
    $VALUES = $values();
  };
}

要点 1: 每个枚举常量都是其所属 enum class 里的一个 public static final 实例

Direction.java 中定义的常量 EAST/WEST/SOUTH/NORTHclass 文件中是什么呢?

Java Language Specification 中的 8.9.3. Enum Members 小节 中提到 ⬇️

For each enum constant c declared in the body of the declaration of E, E has an implicitly declared public static final field of type E that has the same name as c.

image.png

由此可见,EAST/WEST/SOUTH/NORTH 应该是 Direction 这个 class 里的静态字段。 在 javap -p Direction 命令提供的结果中,确实可以找到以下 4 个字段,它们都是 Direction 的实例 ⬇️

  • public static final Direction EAST
  • public static final Direction WEST
  • public static final Direction SOUTH
  • public static final Direction NORTH

4 个字段在类图中的位置如下 ⬇️

image.png

要点 2: java.lang.Enum 中定义了 name/ordinal 字段,并提供了 name()/ordinal() 方法用于访问这两个字段

Direction.java 中,我们通过枚举常量调用了 name()/ordinal() 方法

Enum.java 里可以看到,java.lang.Enum 里定义了 name/ordinal 字段(为节省空间,其他代码用 "..." 表示,下同)

...
    /**
     * The name of this enum constant, as declared in the enum declaration.
     * Most programmers should use the {@link #toString} method rather than
     * accessing this field.
     */
    private final String name;
...

   /**
     * The ordinal of this enumeration constant (its position
     * in the enum declaration, where the initial constant is assigned
     * an ordinal of zero).
     *
     * Most programmers will have no use for this field.  It is designed
     * for use by sophisticated enum-based data structures, such as
     * {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     */
    private final int ordinal;
...    

这两个字段既是 final 的 ,又是 private 的,这样就有两个问题 ⬇️

  • name/ordinal 字段是什么时候被赋值的?
  • 如何访问 name/ordinal 字段?
name/ordinal 字段是什么时候被赋值的?

javap -v -p Direction 命令的输出中包含了 Direction 的构造函数。 构造函数的部分是这样的 ⬇️

  private Direction(java.lang.String);
    descriptor: (Ljava/lang/String;ILjava/lang/String;)V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=3, locals=4, args_size=4
         0: aload_0
         1: aload_1
         2: iload_2
         3: invokespecial #31                 // Method java/lang/Enum."<init>":(Ljava/lang/String;I)V
         6: aload_0
         7: aload_3
         8: putfield      #35                 // Field desc:Ljava/lang/String;
        11: return
      LineNumberTable:
        line 9: 0
        line 10: 6
        line 11: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   LDirection;
            0      12     3  desc   Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      $enum$name                     synthetic
      $enum$ordinal                  synthetic
      desc
    Signature: #99                          // (Ljava/lang/String;)V

我们可以手动将其转化为对应的 java 代码 ⬇️

  // $enum$name 和 $enum$ordinal 都是编译器合成的参数,在 java 代码中找不到它,
  // 为了便于理解,我把它们也展示出来了
  private Direction(java.lang.String $enum$name, int $enum$ordinal, java.lang.String desc) {
    super($enum$name, $enum$ordinal);
    this.desc = desc;
  }

其中 super($enum$name, $enum$ordinal) 调用了 java.lang.Enum 的构造函数(其实这里是句废话,因为除了 Object class 之外,所有 class 的构造函数都会直接/间接调用其父类的构造函数),我们再去看 java.lang.Enum 的构造函数的逻辑 ⬇️

...
   /**
     * Sole constructor.  Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum class declarations.
     *
     * @param name The name of this enum constant, which is the identifier
     *               used to declare it.
     * @param ordinal The ordinal of this enumeration constant (its position
     *         in the enum declaration, where the initial constant is assigned
     *         an ordinal of zero).
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
...

java.lang.Enum 的构造函数中只对 name/ordinal 字段进行了赋值,没有做其他事情(其实它也会调用自己的父类,即 java.lang.Object 的构造函数,不过源码里没有体现,这里就略去了)。

更新后的任务列表如下 ⬇️

  • name/ordinal 字段是什么时候被赋值的?
    • java.lang.Enum 的构造函数中
  • 如何访问 name/ordinal 字段?
如何访问 name/ordinal 字段?

由于 name/ordinal 字段都是 private 的,所以 Direction (以及其他枚举类)无法直接访问它们。而 Enum.java 中提供了 name()/ordinal() 方法,我们可以通过调用 name()/ordinal() 方法来访问 name/ordinal 字段。由于 name()/ordinal() 方法都是 final 的,这就可以保证 name()/ordinal() 方法总是会返回 this.name/this.ordinal 字段,而子类无法 override name()/ordinal() 方法。

更新后的任务列表如下 ⬇️

  • name/ordinal 字段是什么时候被赋值的?
    • java.lang.Enum 的构造函数中
  • 如何访问 name/ordinal 字段?
    • 可以通过调用 name()/ordinal() 方法来访问 name/ordinal 字段

简单汇总一下相关的信息,可以得到如下的表格 ⬇️

字段是否 private何时被赋值对应的 getter 方法
namejava.lang.Enum 的构造函数中java.lang.Enum 中的 name()
ordinaljava.lang.Enum 的构造函数中java.lang.Enum 中的 ordinal()

要点 3: 枚举类中会出现(隐式声明的) public static E[] values() 方法

Java Language Specification 中的 8.9.3. Enum Members 小节 提到 ⬇️

  • An implicitly declared method public static E[] values(), which returns an array containing the enum constants of E, in the same order as they appear in the body of the declaration of E.

image.png

所以 Direction.class 中会有一个自动生成的 values() 静态方法。

javap -v -p Direction 命令所提供的结果中,可以找到与 values() 方法对应的内容 ⬇️

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

可以认为对应的 java 代码如下 ⬇️

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

但这里的 $VALUES 是什么?

javap -v -p Direction 命令的输出中,可以找到 $VALUES 对应的内容 ⬇️

...
  private static final Direction[] $VALUES;
    descriptor: [LDirection;
    flags: (0x101a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
...

可见 $VALUES 是一个合成静态字段(从 ACC_SYNTHETIC 这个 flag 推知它是合成字段)。 另一个问题随之而来,这个字段是如何被赋值的呢? 既然是静态 final 字段,在 class 文件中,应该只能在 <clinit> 中赋值了,我们去看看 <clinit> 部分 ⬇️

...
  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=5, locals=0, args_size=0
         0: new           #1                  // class Direction
         3: dup
         4: ldc           #72                 // String EAST
         6: iconst_0
         7: ldc           #73                 // String 东
         9: invokespecial #75                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
        12: putstatic     #3                  // Field EAST:LDirection;
        15: new           #1                  // class Direction
        18: dup
        19: ldc           #78                 // String WEST
        21: iconst_1
        22: ldc           #79                 // String 西
        24: invokespecial #75                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
        27: putstatic     #7                  // Field WEST:LDirection;
        30: new           #1                  // class Direction
        33: dup
        34: ldc           #81                 // String SOUTH
        36: iconst_2
        37: ldc           #82                 // String 南
        39: invokespecial #75                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
        42: putstatic     #10                 // Field SOUTH:LDirection;
        45: new           #1                  // class Direction
        48: dup
        49: ldc           #84                 // String NORTH
        51: iconst_3
        52: ldc           #85                 // String 北
        54: invokespecial #75                 // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
        57: putstatic     #13                 // Field NORTH:LDirection;
        60: invokestatic  #87                 // Method $values:()[LDirection;
        63: putstatic     #16                 // Field $VALUES:[LDirection;
        66: return
      LineNumberTable:
        line 2: 0
        line 3: 15
        line 4: 30
        line 5: 45
        line 1: 60
...

我手动把它转化成了 java 代码 ⬇️

static {
  EAST = new Direction("EAST", 0, "东");
  WEST = new Direction("WEST", 1, "西");
  SOUTH = new Direction("SOUTH", 2, "南");
  NORTH = new Direction("NORTH", 3, "北");
    
  $VALUES = $values();
}

现在我们来看 $values() 方法里做了些什么。

  private static Direction[] $values();
    descriptor: ()[LDirection;
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=4, locals=0, args_size=0
         0: iconst_4
         1: anewarray     #1                  // class Direction
         4: dup
         5: iconst_0
         6: getstatic     #3                  // Field EAST:LDirection;
         9: aastore
        10: dup
        11: iconst_1
        12: getstatic     #7                  // Field WEST:LDirection;
        15: aastore
        16: dup
        17: iconst_2
        18: getstatic     #10                 // Field SOUTH:LDirection;
        21: aastore
        22: dup
        23: iconst_3
        24: getstatic     #13                 // Field NORTH:LDirection;
        27: aastore
        28: areturn
      LineNumberTable:
        line 1: 0

$values() 是一个合成方法,我们可以认为它对应的 java 代码是这样的 ⬇️

  // $values() 是合成方法,在 java 代码中找不到它,
  // 为了便于理解,我把它也展示出来了
  private static Direction[] $values() {
    return new Direction[] {EAST, WEST, SOUTH, NORTH};
  }

所以 static 语句块里做的事情可以这么概括 ⬇️

  1. EAST/WEST/SOUTH/NORTH4 个静态字段赋值。
  2. 通过 $VALUES = $values() 这个语句给 $VALUES 这个静态字段赋值。

这里涉及的几个字段/方法的名称比较像,容易看混,我画了张表格来整理相关信息 ⬇️

名称是什么如果是方法的话,方法内的逻辑是什么说明
$values()静态方法return new Direction[]{EAST, WEST, SOUTH, NORTH}平时我们 不会 直接调用它
$VALUES静态字段,类型是 Direction[]-平时我们 不会 直接访问它
values()静态方法return (Direction[]) $VALUES.clone()这个方法是 public 的,我们 可以 调用

小结

在类图中可以看到本文所提到的 3 个要点 ⬇️

image.png

正文完。

其他

画“Enum (枚举类型) 的思维导图”所用到的代码

借助 PlantUML,可以用如下代码画出那张图

@startmindmap
'https://plantuml.com/mindmap-diagram

title <i>Enum</i> (枚举类型) 的思维导图

* <i>Enum</i> (枚举类型)
** 每个枚举常量都是其所属枚举类型里的一个 <i>public static final</i> 实例
** <i>name</i> 和 <i>ordinal</i>
*** <b><i>java.lang.Enum</i></b> 中定义了 <i>name<i> 字段以及 <i>name()</i> 方法
*** <b><i>java.lang.Enum</i></b> 中定义了 <i>ordinal</i> 字段以及 <i>ordinal()</i> 方法
** 枚举类型中会出现(隐式声明的) <i>public static E[] values()</i> 方法


@endmindmap

画“Direction 的类图”所用到的代码

借助 PlantUML,可以用如下代码画出那张图

@startuml

title <i>Direction</i> 的类图
caption <b>注意: 有些字段和方法没有画</b>

abstract class java.lang.Enum<E extends Enum<E>>

enum Direction

java.lang.Enum <|-- Direction : extends java.lang.Enum<Direction>

abstract class java.lang.Enum {
    - final String name
    - final int ordinal
    + final String name()
    + final int ordinal()
    # Enum(String, int)
    + {static} <T extends Enum<T>> T valueOf(Class<T>, String)
}

enum Direction {
  + {static} final Direction EAST
  + {static} final Direction WEST
  + {static} final Direction SOUTH
  + {static} final Direction NORTH
  - final String desc
  - {static} final Direction[] $VALUES
  + {static} Direction[] values()
  + {static} Direction valueOf(String)
  - Direction(String)
  + String getDesc()
  + {static} void main(String[])
  - {static} Direction[] $values()
}

@enduml

参考资料