1. 程式人生 > >java列舉型別的實現原理

java列舉型別的實現原理

Java從JDK1.5開始支援列舉,也就是說,Java一開始是不支援列舉的,就像泛型一樣,都是JDK1.5才加入的新特性。通常一個特性如果在一開始沒有提供,在語言發展後期才新增,會遇到一個問題,就是向後相容性的問題。像Java在1.5中引入的很多特性,為了向後相容,編譯器會幫我們寫的原始碼做很多事情,比如泛型為什麼會擦除型別,為什麼會生成橋接方法,foreach迭代,自動裝箱/拆箱等,這有個術語叫“語法糖”,而編譯器的特殊處理叫“解語法糖”。那麼像列舉也是在JDK1.5中才引入的,又是怎麼實現的呢?

Java在1.5中添加了java.lang.Enum抽象類,它是所有列舉型別基類。提供了一些基礎屬性和基礎方法。同時,對把列舉用作Set和Map也提供了支援,即java.util.EnumSet和java.util.EnumMap。

如何定義列舉型別

比如表示加減乘除操作,我們可以定義如下列舉:

package com.mikan;

/**
 * @author Mikan
 * @date 2015-08-29 12:06
 */
public enum Operator {

    ADD,
    SUBTRACT,
    MULTIPLY,
    DIVIDE

}
上面的列舉定義了四個列舉常量,同時,在列舉中還可以定義普通方法、抽象方法,如下所示:
package com.mikan;

/**
 * @author Mikan
 * @date 2015-08-29 12:06
 */
public enum Operator {

    ADD {
        @Override
        public int calculate(int a, int b) {
            return a + b;
        }
    },
    SUBTRACT {
        @Override
        public int calculate(int a, int b) {
            return a - b;
        }
    },
    MULTIPLY {
        @Override
        public int calculate(int a, int b) {
            return a * b;
        }
    },
    DIVIDE {
        @Override
        public int calculate(int a, int b) {
            if (b == 0) {
                throw new IllegalArgumentException("divisor must not be 0");
            }
            return a / b;
        }
    };

    public abstract int calculate(int a, int b);

}
從上面可以看到,我們基本可以像定義類一樣來定義列舉。我們還可以定義屬性、構造方法等:
package com.mikan;

/**
 * @author Mikan
 * @date 2015-08-29 12:06
 */
public enum Operator {

    ADD ("+") {
        @Override
        public int calculate(int a, int b) {
            return a + b;
        }
    },
    SUBTRACT ("-") {
        @Override
        public int calculate(int a, int b) {
            return a - b;
        }
    },
    MULTIPLY  ("*") {
        @Override
        public int calculate(int a, int b) {
            return a * b;
        }
    },
    DIVIDE ("/") {
        @Override
        public int calculate(int a, int b) {
            if (b == 0) {
                throw new IllegalArgumentException("divisor must not be 0");
            }
            return a / b;
        }
    };

    Operator (String operator) {
        this.operator = operator;
    }

    private String operator;

    public abstract int calculate(int a, int b);

    public String getOperator() {
        return operator;
    }

}

實現原理分析

既然可以像使用普通的類一樣使用列舉,編譯器究竟為我們做了些什麼事呢?要想知道這其中的祕密,最有效的途徑就是檢視生成的位元組碼。下面就來看看上面定義的列舉生成的位元組碼是怎麼樣的。

首先來看看反編譯的基本資訊:

localhost:mikan mikan$ javap Operator.class
Compiled from "Operator.java"
public abstract class com.mikan.Operator extends java.lang.Enum<com.mikan.Operator> {
  public static final com.mikan.Operator ADD;
  public static final com.mikan.Operator SUBTRACT;
  public static final com.mikan.Operator MULTIPLY;
  public static final com.mikan.Operator DIVIDE;
  public static com.mikan.Operator[] values();
  public static com.mikan.Operator valueOf(java.lang.String);
  public abstract int calculate(int, int);
  public java.lang.String getOperator();
  com.mikan.Operator(java.lang.String, int, java.lang.String, com.mikan.Operator$1);
  static {};
}
可以看到,一個列舉在經過編譯器編譯過後,變成了一個抽象類,它繼承了java.lang.Enum;而列舉中定義的列舉常量,變成了相應的public static final屬性,而且其型別就抽象類的型別,名字就是列舉常量的名字,同時我們可以在Operator.class的相同路徑下看到四個內部類的.class檔案com/mikan/Operator$1.class、com/mikan/Operator$2.class、com/mikan/Operator$3.class、com/mikan/Operator$4.class,也就是說這四個命名欄位分別使用了內部類來實現的;同時添加了兩個方法values()和valueOf(String);我們定義的構造方法本來只有一個引數,但卻變成了三個引數;同時還生成了一個靜態程式碼塊。這些具體的內容接下來仔細看看。

看下面詳細的反編譯資訊:

localhost:mikan mikan$ javap -c -v Operator.class
Classfile /Users/mikan/Documents/workspace/project/algorithm/target/classes/com/mikan/Operator.class
  Last modified 2015-8-29; size 1720 bytes
  MD5 checksum 478439554cb827fec3c36cf51c8d36da
  Compiled from "Operator.java"
public abstract class com.mikan.Operator extends java.lang.Enum<com.mikan.Operator>
  Signature: #67                          // Ljava/lang/Enum<Lcom/mikan/Operator;>;
  SourceFile: "Operator.java"
  InnerClasses:
       static #24; //class com/mikan/Operator$4
       static #19; //class com/mikan/Operator$3
       static #14; //class com/mikan/Operator$2
       static #9; //class com/mikan/Operator$1
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT, ACC_ENUM
Constant pool:
  // 省略常量池資訊
{
  public static final com.mikan.Operator ADD;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
  public static final com.mikan.Operator SUBTRACT;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
  public static final com.mikan.Operator MULTIPLY;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
  public static final com.mikan.Operator DIVIDE;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
  public static com.mikan.Operator[] values();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #2                  // Field $VALUES:[Lcom/mikan/Operator;
         3: invokevirtual #3                  // Method "[Lcom/mikan/Operator;".clone:()Ljava/lang/Object;
         6: checkcast     #4                  // class "[Lcom/mikan/Operator;"
         9: areturn
      LineNumberTable:
        line 7: 0

  public static com.mikan.Operator valueOf(java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc_w         #5                  // class com/mikan/Operator
         3: aload_0
         4: invokestatic  #6                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         7: checkcast     #5                  // class com/mikan/Operator
        10: areturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      11     0  name   Ljava/lang/String;

  public abstract int calculate(int, int);
    flags: ACC_PUBLIC, ACC_ABSTRACT

  public java.lang.String getOperator();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #8                  // Field operator:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 46: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   Lcom/mikan/Operator;

  com.mikan.Operator(java.lang.String, int, java.lang.String, com.mikan.Operator$1);
    flags: ACC_SYNTHETIC
    Code:
      stack=4, locals=5, args_size=5
         0: aload_0
         1: aload_1
         2: iload_2
         3: aload_3
         4: invokespecial #1                  // Method "<init>":(Ljava/lang/String;ILjava/lang/String;)V
         7: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       8     0  this   Lcom/mikan/Operator;
               0       8     1    x0   Ljava/lang/String;
               0       8     2    x1   I
               0       8     3    x2   Ljava/lang/String;
               0       8     4    x3   Lcom/mikan/Operator$1;

  static {};
    flags: ACC_STATIC
    Code:
      stack=5, locals=0, args_size=0
         0: new           #9                  // class com/mikan/Operator$1
         3: dup
         4: ldc           #10                 // String ADD
         6: iconst_0
         7: ldc           #11                 // String +
         9: invokespecial #12                 // Method com/mikan/Operator$1."<init>":(Ljava/lang/String;ILjava/lang/String;)V
        12: putstatic     #13                 // Field ADD:Lcom/mikan/Operator;
        15: new           #14                 // class com/mikan/Operator$2
        18: dup
        19: ldc           #15                 // String SUBTRACT
        21: iconst_1
        22: ldc           #16                 // String -
        24: invokespecial #17                 // Method com/mikan/Operator$2."<init>":(Ljava/lang/String;ILjava/lang/String;)V
        27: putstatic     #18                 // Field SUBTRACT:Lcom/mikan/Operator;
        30: new           #19                 // class com/mikan/Operator$3
        33: dup
        34: ldc           #20                 // String MULTIPLY
        36: iconst_2
        37: ldc           #21                 // String *
        39: invokespecial #22                 // Method com/mikan/Operator$3."<init>":(Ljava/lang/String;ILjava/lang/String;)V
        42: putstatic     #23                 // Field MULTIPLY:Lcom/mikan/Operator;
        45: new           #24                 // class com/mikan/Operator$4
        48: dup
        49: ldc           #25                 // String DIVIDE
        51: iconst_3
        52: ldc           #26                 // String /
        54: invokespecial #27                 // Method com/mikan/Operator$4."<init>":(Ljava/lang/String;ILjava/lang/String;)V
        57: putstatic     #28                 // Field DIVIDE:Lcom/mikan/Operator;
        60: iconst_4
        61: anewarray     #5                  // class com/mikan/Operator
        64: dup
        65: iconst_0
        66: getstatic     #13                 // Field ADD:Lcom/mikan/Operator;
        69: aastore
        70: dup
        71: iconst_1
        72: getstatic     #18                 // Field SUBTRACT:Lcom/mikan/Operator;
        75: aastore
        76: dup
        77: iconst_2
        78: getstatic     #23                 // Field MULTIPLY:Lcom/mikan/Operator;
        81: aastore
        82: dup
        83: iconst_3
        84: getstatic     #28                 // Field DIVIDE:Lcom/mikan/Operator;
        87: aastore
        88: putstatic     #2                  // Field $VALUES:[Lcom/mikan/Operator;
        91: return
      LineNumberTable:
        line 9: 0
        line 15: 15
        line 21: 30
        line 27: 45
        line 7: 60
}
localhost:mikan mikan$
下面分析一下位元組碼中的各部分,其中:
  InnerClasses:
       static #24; //class com/mikan/Operator$4
       static #19; //class com/mikan/Operator$3
       static #14; //class com/mikan/Operator$2
       static #9; //class com/mikan/Operator$1
從中可以看到它有4個內部類,這四個內部類的詳細資訊後面會分析。

靜態程式碼塊:

  static {};
    flags: ACC_STATIC
    Code:
      stack=5, locals=0, args_size=0
// 建立一個Operator$1的內部類物件
         0: new           #9                  // class com/mikan/Operator$1
         3: dup
// 接下來的三條指令分別是把三個引數推送到棧頂,然後呼叫Operator$1的編譯器生成的<init>方法
         4: ldc           #10                 // String ADD
         6: iconst_0
         7: ldc           #11                 // String +
// 呼叫<init>方法
         9: invokespecial #12                 // Method com/mikan/Operator$1."<init>":(Ljava/lang/String;ILjava/lang/String;)V
// 設定ADD屬性的值為新建立的物件
        12: putstatic     #13                 // Field ADD:Lcom/mikan/Operator;
// 接下來是分別初始化另外三個屬性SUBTRACT、MULTIPLY、DIVIDE,這裡就不再重複
        15: new           #14                 // class com/mikan/Operator$2
        18: dup
        19: ldc           #15                 // String SUBTRACT
        21: iconst_1
        22: ldc           #16                 // String -
        24: invokespecial #17                 // Method com/mikan/Operator$2."<init>":(Ljava/lang/String;ILjava/lang/String;)V
        27: putstatic     #18                 // Field SUBTRACT:Lcom/mikan/Operator;
        30: new           #19                 // class com/mikan/Operator$3
        33: dup
        34: ldc           #20                 // String MULTIPLY
        36: iconst_2
        37: ldc           #21                 // String *
        39: invokespecial #22                 // Method com/mikan/Operator$3."<init>":(Ljava/lang/String;ILjava/lang/String;)V
        42: putstatic     #23                 // Field MULTIPLY:Lcom/mikan/Operator;
        45: new           #24                 // class com/mikan/Operator$4
        48: dup
        49: ldc           #25                 // String DIVIDE
        51: iconst_3
        52: ldc           #26                 // String /
        54: invokespecial #27                 // Method com/mikan/Operator$4."<init>":(Ljava/lang/String;ILjava/lang/String;)V
        57: putstatic     #28                 // Field DIVIDE:Lcom/mikan/Operator;
// 下面是new了一個長度為4的Operator型別的陣列,並分別設定陣列中各元素的值為上面的四個屬性的值
        60: iconst_4
        61: anewarray     #5                  // class com/mikan/Operator
        64: dup
        65: iconst_0
        66: getstatic     #13                 // Field ADD:Lcom/mikan/Operator;
        69: aastore
        70: dup
        71: iconst_1
        72: getstatic     #18                 // Field SUBTRACT:Lcom/mikan/Operator;
        75: aastore
        76: dup
        77: iconst_2
        78: getstatic     #23                 // Field MULTIPLY:Lcom/mikan/Operator;
        81: aastore
        82: dup
        83: iconst_3
        84: getstatic     #28                 // Field DIVIDE:Lcom/mikan/Operator;
        87: aastore
//下面是設定屬性$VALUES的值為剛建立的陣列
        88: putstatic     #2                  // Field $VALUES:[Lcom/mikan/Operator;
        91: return
其實編譯器生成的這個靜態程式碼塊做了如下工作:分別設定生成的四個公共靜態常量欄位的值,同時編譯器還生成了一個靜態欄位$VALUES,儲存的是列舉型別定義的所有列舉常量。相當於下面的程式碼:
Operator ADD = new Operator1();
Operator SUBTRACT = new Operator1();
Operator MULTIPLY = new Operator1();
Operator DIVIDE = new Operator1();
Operator[] $VALUES = new Operator[4];
$VALUES[0] = ADD;
$VALUES[1] = SUBTRACT;
$VALUES[2] = MULTIPLY;
$VALUES[3] = DIVIDE;
編譯器新增的values方法:
  public static com.mikan.Operator[] values();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #2                  // Field $VALUES:[Lcom/mikan/Operator;
         3: invokevirtual #3                  // Method "[Lcom/mikan/Operator;".clone:()Ljava/lang/Object;
         6: checkcast     #4                  // class "[Lcom/mikan/Operator;"
         9: areturn
這個方法是一個公共的靜態方法,所以我們可以直接呼叫該方法(Operator.values()),返回這個列舉值的陣列,另外,這個方法的實現是,克隆在靜態程式碼塊中初始化的$VALUES欄位的值,並把型別強轉成Operator[]型別返回。它相當於下面的程式碼:
public static com.mikan.Operator[] values() {
return (Operator[])$VALUES.clone();
}
編譯器新增的valueOf方法:
  public static com.mikan.Operator valueOf(java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc_w         #5                  // class com/mikan/Operator
         3: aload_0
         4: invokestatic  #6                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         7: checkcast     #5                  // class com/mikan/Operator
        10: areturn
這個方法是一個公共的靜態方法,所以我們可以直接呼叫該方法(Operator.valueOf()),返回引數字串表示的列舉常量,另外,這個方法的實現是,呼叫父類Enum的valueOf方法,並把型別強轉成Operator。它相當於如下的程式碼:
public static com.mikan.Operator valueOf(String name) {
return (Operator)Enum.valueOf(Operator.class, name);
}
生成的內部類

下面看看生成的內部類Operator$1:

localhost:mikan mikan$ javap Operator\$1.class
Compiled from "Operator.java"
final class com.mikan.Operator$1 extends com.mikan.Operator {
  com.mikan.Operator$1(java.lang.String, int, java.lang.String);
  public int calculate(int, int);
}
localhost:mikan mikan$
可以看到,實現內部類是繼承自Operator,即
    ADD {
        @Override
        public int calculate(int a, int b) {
            return a + b;
        }
    },
這就是說,我們定義的每個列舉常量,最終都生成了一個像上面這樣的內部類。

構造方法為什麼增加了兩個引數?

有一個問題,構造方法我們明明只定義了一個引數,為什麼生成的構造方法是三個引數呢?

從Enum類中我們可以看到,為每個列舉都定義了兩個屬性,name和ordinal,name表示我們定義的列舉常量的名稱,如ADD、SUBTRACT等,而ordinal是一個順序號,根據定義的順序分別賦予一個整形值,從0開始。在列舉常量初始化時,會自動為初始化這兩個欄位,設定相應的值,所以才在構造方法中添加了兩個引數。即:

  com.mikan.Operator$1(String name, int ordinal, String operator);
另外三個列舉常量生成的內部類基本上差不多,這裡就不重複說明了。

我們可以從Enum類的程式碼中看到,定義的name和ordinal屬性都是final的,而且大部分方法也都是final的,特別是clone、readObject、writeObject這三個方法,這三個方法和列舉通過靜態程式碼塊來進行初始化一起,它保證了列舉型別的不可變性,不能通過克隆,不能通過序列化和反序列化來複制列舉,這能保證一個列舉常量只是一個例項,即是單例的,所以在effective java中推薦使用列舉來實現單例。

總結

列舉本質上是通過普通的類來實現的,只是編譯器為我們進行了處理。每個列舉型別都繼承自java.lang.Enum,並自動添加了values和valueOf方法。而每個列舉常量是一個靜態常量欄位,使用內部類實現,該內部類繼承了列舉類。所有列舉常量都通過靜態程式碼塊來進行初始化,即在類載入期間就初始化。另外通過把clone、readObject、writeObject這三個方法定義為final的,同時實現是丟擲相應的異常。這樣保證了每個列舉型別及列舉常量都是不可變的。可以利用列舉的這兩個特性來實現執行緒安全的單例。