深入JVM字節(jié)碼探索switch指令、字符串褪尝、枚舉

引言

從 C 到 C++ 到 Java 到一系列各種各樣的語言闹获,大多都支持多路分支語句期犬,比如 Kotlin 的when和 Rust 的match等等,在 Java SE 14 版本的語言規(guī)范也添加了對switch表達式的支持避诽。

本文主要針對 Java SE 8 版本中的switch語句從字節(jié)碼層面進行研究哭懈,理解switch語句相關的各種細節(jié),并嘗試著對其編譯產物人工地進行反編譯茎用,探索字符串switch和枚舉switch的具體實現(xiàn)方式遣总。

目錄

  1. switch關鍵字基礎
  2. switch所對應的兩種指令
  3. switch字符串的實現(xiàn)原理
  4. switch枚舉的實現(xiàn)原理

1. switch關鍵字基礎

首先,引用一下語言規(guī)范中的下面幾句話:

The switch Statement

The switch Statement

The switch statement transfers control to one of several statements depending on the value of an expression.

The type of the Expression must be char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type, or a compile-time error occurs.

When the switch statement is executed, first the Expression is evaluated. If the Expression evaluates to null, a NullPointerException is thrown and the entire switch statement completes abruptly for that reason. Otherwise, if the result is of a reference type, it is subject to unboxing conversion.

這里提到switch所接收的表達式參數(shù)必須為char轨功、byte旭斥、shortint古涧、Character垂券、ByteShort羡滑、Integer菇爪、StringEnum柒昏,若該表達式參數(shù)為null凳宙,則拋出 NPE,否則若為引用類型則需要進行拆箱轉換职祷。

實際上switch在字節(jié)碼層面可以認為它僅僅只支持int一種類型氏涩,下文我會對此進行解釋。

然后我們可以在這里注意到幾個細節(jié):

  1. 這里的參數(shù)類型應是編譯期確定的類型有梆,而不是運行時類型是尖。

  2. 這里的參數(shù)在運行時不可為null,否則將拋出 NPE泥耀。

  3. 支持Character饺汹、ByteShort痰催、Integer這四種類型的參數(shù)

    在編譯期將分別對這四種類型的參數(shù)通過charValue兜辞、byteValueshortValue陨囊、intValue這四個方法轉變?yōu)?code>char弦疮、byte夹攒、short蜘醋、int類型,也就是說實質上對于Character咏尝、Byte压语、Short啸罢、Integerswitch實質上仍舊是對于charbyte胎食、short扰才、intswitch

  4. switch不支持Long厕怜、Double衩匣、FloatBoolean類型的參數(shù)

    通過對上一條的理解粥航,switch不支持Long琅捏、DoubleFloat递雀、Boolean類型的原因應該是因為switch不支持long柄延、doublefloat缀程、boolean搜吧。

  5. switch不支持longdouble杨凑、float類型的參數(shù)

    因為從long滤奈、doublefloat類型向int類型進行轉換可能會造成損失撩满,所以編譯期不會輕易地將它們隱式轉換為int類型僵刮,我們只能在自己的源代碼中手動地進行顯式強制類型轉換,才可以將它們轉為int類型鹦牛。

  6. switch不支持boolean類型的參數(shù)

    booleanint的轉換是沒有損失的搞糕,但是實際上我們并沒有用switchboolean類型的參數(shù)進行多路分支的必要,畢竟我們可以直接使用if語句曼追。

到此為止窍仰,我們遺留了幾個主要的問題:

  1. switch是如何支持int類型的

  2. switch如何依賴對int的支持而提供對charbyte礼殊、short的支持

  3. switch指令如何進行多路分支的跳轉

  4. switch如何依賴對int的支持而提供對String的支持

  5. switch如何依賴對int的支持而提供對Enum的支持

下文將對以上問題進行討論與解答驹吮。

2. switch所對應的兩種指令

Compiling Switches

Compilation of switch statements uses the tableswitch and lookupswitch instructions.

The Java Virtual Machine's tableswitch and lookupswitch instructions operate only on int data. Because operations on byte, char, or short values are internally promoted to int, a switch whose expression evaluates to one of those types is compiled as though it evaluated to type int.

switch語句要使用tableswitchlookupswitch這兩個指令,這兩個指令只針對int類型進行操作晶伦,而char碟狞、byteshort這三種類型將被隱式轉換為int婚陪。

如果熟悉 JVM 字節(jié)碼指令集族沃,那么應該很容易理解這兩種switch僅僅支持int類型的原因,事實上 JVM 中許多操作都沒有對每種基本類型都專門設計單獨的指令,這是因為 JVM 的所有指令都僅有一個字節(jié)而已脆淹,這樣的好處是不必進行對齊常空,因此效率比較高,但是其弊端就是最多只能提供 256 種指令盖溺,假如真的讓所有操作都同時對boolean漓糙、charfloat烘嘱、double昆禽、byteshort蝇庭、int为狸、long以及引用這 9 種類型都提供支持,那么 JVM 將最多只能支持二三十種操作遗契,這絕對是不夠用的辐棒,因此許多的操作都僅僅只支持其中的一部分類型,而其余的類型將在運行過程中經(jīng)過類型轉換牍蜂。

2.1 tableswitch

對以下程序進行編譯然后進行反編譯:

class Test {
    static int test(int var0) {
        switch (var0) {
            case 0:
                return 0;
            case 2:
                return 2;
            case 3:
                return 3;
            default:
                return -1;
        }
    }
}
Compiled from "Test.java"
class Test {
  Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static int test(int);
    Code:
       0: iload_0
       1: tableswitch   { // 0 to 3
                     0: 32
                     1: 38
                     2: 34
                     3: 36
               default: 38
          }
      32: iconst_0
      33: ireturn
      34: iconst_2
      35: ireturn
      36: iconst_3
      37: ireturn
      38: iconst_m1
      39: ireturn
}

上面這個類的文件在我編譯后是這樣的:

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: CA FE BA BE 00 00 00 37 00 10 0A 00 03 00 0D 07    J~:>...7........
00000010: 00 0E 07 00 0F 01 00 06 3C 69 6E 69 74 3E 01 00    ........<init>..
00000020: 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69    .()V...Code...Li
00000030: 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 04    neNumberTable...
00000040: 74 65 73 74 01 00 04 28 49 29 49 01 00 0D 53 74    test...(I)I...St
00000050: 61 63 6B 4D 61 70 54 61 62 6C 65 01 00 0A 53 6F    ackMapTable...So
00000060: 75 72 63 65 46 69 6C 65 01 00 09 54 65 73 74 2E    urceFile...Test.
00000070: 6A 61 76 61 0C 00 04 00 05 01 00 04 54 65 73 74    java........Test
00000080: 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A    ...java/lang/Obj
00000090: 65 63 74 00 20 00 02 00 03 00 00 00 00 00 02 00    ect.............
000000a0: 00 00 04 00 05 00 01 00 06 00 00 00 1D 00 01 00    ................
000000b0: 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 07    .....*7..1......
000000c0: 00 00 00 06 00 01 00 00 00 01 00 08 00 08 00 09    ................
000000d0: 00 01 00 06 00 00 00 5C 00 01 00 01 00 00 00 28    .......\.......(
000000e0: 1A AA 00 00 00 00 00 25 00 00 00 00 00 00 00 03    .*.....%........
000000f0: 00 00 00 1F 00 00 00 25 00 00 00 21 00 00 00 23    .......%...!...#
00000100: 03 AC 05 AC 06 AC 02 AC 00 00 00 02 00 07 00 00    .,.,.,.,........
00000110: 00 16 00 05 00 00 00 03 00 20 00 05 00 22 00 07    ............."..
00000120: 00 24 00 09 00 26 00 0B 00 0A 00 00 00 06 00 04    .$...&..........
00000130: 20 01 01 01 00 01 00 0B 00 00 00 02 00 0C          ..............

從 0x0x000000e1 至 0x0x000000ff 的這 31 個字節(jié)便是tableswitch漾根。

按照tableswitch的解釋,tableswitch即 0xAA鲫竞,其后分別跟隨 default辐怕、low、high从绘,在本例中即分別為37寄疏、0、3僵井,再其后跟隨其余的 high - low + 1 即 4 個 offset陕截,在本例中分別為 31、37批什、33农曲、35。

這里需要注意一個細節(jié)驻债,那就是在 0xAA 與 default 之間存在 0 至 3 個填充字節(jié)乳规,目的在于保證 default、low合呐、high 以及后面的每一個 offset 在 class 文件中相對于這個方法的第一個指令的地址偏移都是 4 的倍數(shù)暮的,在本例中填充字節(jié)有 2 個,該方法的第一個指令的地址是 0x000000e0淌实。

tableswitchint參數(shù)從操作數(shù)棧中彈出冻辩,并直接查表然后跳轉猖腕。此處應注意到上面的源碼中并不存在case 1,但是在字節(jié)碼中卻在case 1的位置存在一個和 default 相等的值微猖。

這是因為tableswitch需要直接查表獲取即將跳轉的地址偏移谈息,這個偏移從 0xAA 的位置開始計算缘屹,所以這個映射表必須支持隨機讀取凛剥,也就是說所有的case都要在 low 至 high 的范圍內順序排列,若參數(shù)小于 low 或大于 high 則跳轉到 default 的位置轻姿,而在 table 內部被填充的表項也對應此 default 值犁珠。

tableswitch看起來應十分高效,因為虛擬機可以直接查表從而得到對應的 offset互亮,但是這里也有一個缺陷犁享,也就是上一段剛剛提到的,需要保證每個case的連續(xù)豹休,即使源碼中并不存在這個case炊昆,也要在編譯后填充。因此tableswitch僅適用于switchcase相對來說比較密集的情況下威根,而在其比較稀疏的情況下則不應使用tableswitch而應使用lookupswitch凤巨。

2.2 lookupswitch

對以下程序進行編譯然后進行反編譯:

class Test {
    static int test(int var0) {
        switch (var0) {
            case 1000:
                return 0;
            case 100:
                return 1;
            case 10:
                return 2;
            default:
                return -1;
        }
    }
}
Compiled from "Test.java"
class Test {
  Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static int test(int);
    Code:
       0: iload_0
       1: lookupswitch  { // 3
                    10: 40
                   100: 38
                  1000: 36
               default: 42
          }
      36: iconst_0
      37: ireturn
      38: iconst_1
      39: ireturn
      40: iconst_2
      41: ireturn
      42: iconst_m1
      43: ireturn
}

上面這個類的文件在我編譯后是這樣的:

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: CA FE BA BE 00 00 00 37 00 10 0A 00 03 00 0D 07    J~:>...7........
00000010: 00 0E 07 00 0F 01 00 06 3C 69 6E 69 74 3E 01 00    ........<init>..
00000020: 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69    .()V...Code...Li
00000030: 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 04    neNumberTable...
00000040: 74 65 73 74 01 00 04 28 49 29 49 01 00 0D 53 74    test...(I)I...St
00000050: 61 63 6B 4D 61 70 54 61 62 6C 65 01 00 0A 53 6F    ackMapTable...So
00000060: 75 72 63 65 46 69 6C 65 01 00 09 54 65 73 74 2E    urceFile...Test.
00000070: 6A 61 76 61 0C 00 04 00 05 01 00 04 54 65 73 74    java........Test
00000080: 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A    ...java/lang/Obj
00000090: 65 63 74 00 20 00 02 00 03 00 00 00 00 00 02 00    ect.............
000000a0: 00 00 04 00 05 00 01 00 06 00 00 00 1D 00 01 00    ................
000000b0: 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 07    .....*7..1......
000000c0: 00 00 00 06 00 01 00 00 00 01 00 08 00 08 00 09    ................
000000d0: 00 01 00 06 00 00 00 60 00 01 00 01 00 00 00 2C    .......`.......,
000000e0: 1A AB 00 00 00 00 00 29 00 00 00 03 00 00 00 0A    .+.....)........
000000f0: 00 00 00 27 00 00 00 64 00 00 00 25 00 00 03 E8    ...'...d...%...h
00000100: 00 00 00 23 03 AC 04 AC 05 AC 02 AC 00 00 00 02    ...#.,.,.,.,....
00000110: 00 07 00 00 00 16 00 05 00 00 00 03 00 24 00 05    .............$..
00000120: 00 26 00 07 00 28 00 09 00 2A 00 0B 00 0A 00 00    .&...(...*......
00000130: 00 06 00 04 24 01 01 01 00 01 00 0B 00 00 00 02    ....$...........
00000140: 00 0C                                              ..

從 0x0x000000e1 至 0x0x00001003 的這 35 個字節(jié)便是lookupswitch

按照lookupswitch的解釋洛搀,lookupswitch即0xAB敢茁,其后分別跟隨 default、npairs留美,在本例中即分別為41彰檬、3,再其后跟隨其余的 npairs 即 3 組映射谎砾,在本例中分別為10逢倍、39、100景图、37瓶堕、1000、35症歇。

lookupswitch也需要填充 0 至 3 個字節(jié)郎笆,本例中是 2 個,該方法第一個指令的地址為 0x000000e0忘晤。

lookupswitchint參數(shù)也從操作數(shù)棧中彈出宛蚓,并在這些case中查找匹配項。在我上面的源碼中case順序是 1000设塔、100凄吏、10,但是在字節(jié)碼中卻是 10、100痕钢、1000图柏,這是因為lookupswitch要求所有 offset 應以遞增的順序排列,從而使 JVM 可以支持比線性更高效的查找方式任连,比如二分查找蚤吹,但是規(guī)范中在此并沒有要求虛擬機所實現(xiàn)的查找方式。

由于lookupswitch需要進行查找随抠,而不能像tableswitch那樣直接查表裁着,因此或許效率會有所降低,但是在case相對比較稀疏的情況下拱她,比起tableswitch二驰,lookupswitch將節(jié)省大量的空間。

對于我們在代碼中的switch語句秉沼,選擇lookupswitch或是tableswitch將依賴于編譯器的具體實現(xiàn)桶雀。

3. switch字符串的實現(xiàn)原理

以下代碼作為示例:

class Test {
    static int test(String var0) {
        switch (var0) {
            case "foo":
                return 0;
            case "bar":
                return 1;
            case "10":
                return 2;
            case "0O":
                return 3;
            default:
                return -1;
        }
    }
}

編譯后的字節(jié)碼:

Compiled from "Test.java"
class Test {
  Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static int test(java.lang.String);
    Code:
       0: aload_0
       1: astore_1
       2: iconst_m1
       3: istore_2
       4: aload_1
       5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
       8: lookupswitch  { // 3
                  1567: 72
                 97299: 58
                101574: 44
               default: 97
          }
      44: aload_1
      45: ldc           #3                  // String foo
      47: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      50: ifeq          97
      53: iconst_0
      54: istore_2
      55: goto          97
      58: aload_1
      59: ldc           #5                  // String bar
      61: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      64: ifeq          97
      67: iconst_1
      68: istore_2
      69: goto          97
      72: aload_1
      73: ldc           #6                  // String 0O
      75: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      78: ifeq          86
      81: iconst_3
      82: istore_2
      83: goto          97
      86: aload_1
      87: ldc           #7                  // String 10
      89: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      92: ifeq          97
      95: iconst_2
      96: istore_2
      97: iload_2
      98: tableswitch   { // 0 to 3
                     0: 128
                     1: 130
                     2: 132
                     3: 134
               default: 136
          }
     128: iconst_0
     129: ireturn
     130: iconst_1
     131: ireturn
     132: iconst_2
     133: ireturn
     134: iconst_3
     135: ireturn
     136: iconst_m1
     137: ireturn
}

人工地反編譯:

class Test {
    static int test(String var0) {
        String var1 = var0;
        int var2 = -1;
        switch (var1.hashCode()) {
            case 101574:
                if (var1.equals("foo")) {
                    var2 = 0;
                }
                break;
            case 97299:
                if (var1.equals("bar")) {
                    var2 = 1;
                }
                break;
            case 1567:
                if (var1.equals("0O")) {
                    var2 = 3;
                } else if (var1.equals("10")) {
                    var2 = 2;
                }
                break;
        }
        switch (var2) {
            case 0:
                return 0;
            case 1:
                return 1;
            case 2:
                return 2;
            case 3:
                return 3;
            default:
                return -1;
        }
    }
}

在我的環(huán)境下,這和上面的字符串switch編譯結果幾乎完全一致唬复,javap -c無任何區(qū)別矗积。

對字符串的switch實際上被拆分成了 5 個步驟:

  1. 調用參數(shù)字符串的hashCode方法獲取 hash 值

  2. 對其 hash 值進行第一次switch

  3. 調用equals方法并修改狀態(tài)碼

  4. 對狀態(tài)碼進行第二次switch

  5. 執(zhí)行對應分支中的語句

這里注意幾個細節(jié):

  1. 若參數(shù)字符串為null,則調用hashCode方法將拋出 NPE盅抚,符合虛擬機規(guī)范的要求漠魏。

  2. 狀態(tài)碼的默認值為 -1,若參數(shù)字符串沒有匹配任何一個case字面量妄均,則會保持不變柱锹,否則將被修改為對應case在源代碼中的序號,從 0 開始計算丰包。

  3. case字符串字面量的 hash 值需要在編譯期經(jīng)過計算并寫入 class 字節(jié)碼文件中禁熏,而參數(shù)字符串的hashCode方法需要在運行時調用才能夠得到結果,這就要求同一個字符串的 hash 算法必須在編譯期和運行時是對應的邑彪,否則經(jīng)過第一次switch后狀態(tài)碼將被賦予錯誤的值瞧毙,于是在第二次switch將走入錯誤的分支路徑中,執(zhí)行錯誤的邏輯寄症。

    由于虛擬機會對被加載的類進行版本驗證宙彪,因此 hash 算法的一致在類加載的流程中可以被虛擬機所保證。

  4. 僅對參數(shù)字符串的 hash 進行switch不足以確定其是否與case字面量匹配有巧,因此至少會調用一次equals方法释漆。

    比如上例中"0O""10"這兩個字符串的 hash 值相同,所以在完成對 hash 值的switch后篮迎,要以這兩個case的倒序逐個調用equals方法并判斷結果男图。

    此處可以參考 Java 8 中java.lang.String的源碼:

/**
    * Returns a hash code for this string. The hash code for a
    * {@code String} object is computed as
    * <blockquote><pre>
    * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
    * </pre></blockquote>
    * using {@code int} arithmetic, where {@code s[i]} is the
    * <i>i</i>th character of the string, {@code n} is the length of
    * the string, and {@code ^} indicates exponentiation.
    * (The hash value of the empty string is zero.)
    *
    * @return  a hash code value for this object.
    */
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

自定義類的 hash 算法在編譯期是無法確定的示姿,因此switch不能以和字符串字面量相同的方式實現(xiàn)自定義類的多路分支。

4. switch枚舉的實現(xiàn)原理

以下代碼作為示例:

enum Foobar {
    FOO,
    BAR;
}
class Test {
    static int test(Foobar var0) {
        switch (var0) {
            case FOO:
                return 1;
            case BAR:
                return 2;
            default:
                return 0;
        }
    }
}

編譯后的字節(jié)碼:

Compiled from "Test.java"
class Test {
  Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static int test(Foobar);
    Code:
       0: getstatic     #2                  // Field Test$1.$SwitchMap$Foobar:[I
       3: aload_0
       4: invokevirtual #3                  // Method Foobar.ordinal:()I
       7: iaload
       8: lookupswitch  { // 2
                     1: 36
                     2: 38
               default: 40
          }
      36: iconst_1
      37: ireturn
      38: iconst_2
      39: ireturn
      40: iconst_0
      41: ireturn
}
Compiled from "Test.java"
class Test$1 {
  static final int[] $SwitchMap$Foobar;

  static {};
    Code:
       0: invokestatic  #1                  // Method Foobar.values:()[LFoobar;
       3: arraylength
       4: newarray       int
       6: putstatic     #2                  // Field $SwitchMap$Foobar:[I
       9: getstatic     #2                  // Field $SwitchMap$Foobar:[I
      12: getstatic     #3                  // Field Foobar.FOO:LFoobar;
      15: invokevirtual #4                  // Method Foobar.ordinal:()I
      18: iconst_1
      19: iastore
      20: goto          24
      23: astore_0
      24: getstatic     #2                  // Field $SwitchMap$Foobar:[I
      27: getstatic     #6                  // Field Foobar.BAR:LFoobar;
      30: invokevirtual #4                  // Method Foobar.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
}

人工地反編譯:

class Test {
    static int test(Foobar var0) {
        switch (Test$1.$SwitchMap$Foobar[var0.ordinal()]) {
            case 1:
                return 1;
            case 2:
                return 2;
            default:
                return 0;
        }
    }
}
class Test$1 {
    static final int[] $SwitchMap$Foobar;
    static {
        $SwitchMap$Foobar = new int[Foobar.values().length];
        try {
            $SwitchMap$Foobar[Foobar.FOO.ordinal()] = 1;
        } catch (NoSuchFieldError e) {
            ;
        }
        try {
            $SwitchMap$Foobar[Foobar.BAR.ordinal()] = 2;
        } catch (NoSuchFieldError e) {
            ;
        }
    }
}

在我的環(huán)境下逊笆,這和上面的枚舉switch編譯結果幾乎完全一致栈戳,唯一的不同之處是在自動生成的那個匿名內部類中并沒有包私有無參構造函數(shù),但手動編寫的類會被自動添加一個包私有無參構造函數(shù)难裆。

這個 Test$1 類是自動生成的包私有靜態(tài)匿名內部類子檀,匿名類的名字都是其外部類的名字加'$'加數(shù)字,'$'在每層嵌套類的名字之間作為間隔符差牛,數(shù)字則是為匿名類自動生成的名字命锄,其值在編譯期被確定堰乔,因此可能在迭代過程中隨著項目版本的變化而變化偏化,本例中這個數(shù)字為 1。

這里可以發(fā)現(xiàn)镐侯,在 Test 類中對 Foobar 枚舉的switch被轉變成了對 Test$1.$SwitchMap$Foobar[var0.ordinal()] 這一整數(shù)表達式的switch侦讨,這里 Test$1 的靜態(tài)成員 $SwitchMap$Foobar 也在編譯期自動生成,這是一個int數(shù)組苟翻,被選取的參數(shù)在這個數(shù)組中的索引是這個 Foobar 對象的ordinal方法的返回值韵卤。

ordinal方法的返回值是這個枚舉常量在其聲明時的序號。這里參考一下Enum類的源碼:

/**
 * 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;

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

這里注意若枚舉參數(shù)為null則調用ordinal方法會拋出 NPE崇猫,符合虛擬機規(guī)范的要求沈条。

$SwitchMap$Foobar 數(shù)組的長度與在 Test 類中所依賴的那個版本的枚舉 Foobar 類的枚舉常量個數(shù)相同,這個數(shù)組在這個自動生成的包私有靜態(tài)匿名內部類被加載時被初始化诅炉,分別取出在 Test 類中所依賴的那個版本的枚舉 Foobar 類的所有枚舉常量蜡歹,并在它們的ordinal的位置設置ordinal + 1的值。

因此涕烧,當在 Test 類中對 Test$1.$SwitchMap$Foobar[var0.ordinal()] 這個int類型的表達式進行switch時月而,若這個int參數(shù)為正數(shù),則可以跳轉到所對應的路徑议纯,并執(zhí)行對應的語句父款,否則將跳轉到default分支,在這種情況下參數(shù)通常為默認值0瞻凤。

這里要注意一下憨攒,在 Test$1 類中調用了 Foobar 類的靜態(tài)方法values,它是在 Foobar 被編譯時自動生成的阀参。

這里稍微分析一下 Foobar 這個枚舉類:

源代碼:

enum Foobar {
    FOO,
    BAR;
}

字節(jié)碼:

Compiled from "Foobar.java"
final class Foobar extends java.lang.Enum<Foobar> {
  public static final Foobar FOO;

  public static final Foobar BAR;

  public static Foobar[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[LFoobar;
       3: invokevirtual #2                  // Method "[LFoobar;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[LFoobar;"
       9: areturn

  public static Foobar valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class Foobar
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class Foobar
       9: areturn

  static {};
    Code:
       0: new           #4                  // class Foobar
       3: dup
       4: ldc           #7                  // String FOO
       6: iconst_0
       7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #9                  // Field FOO:LFoobar;
      13: new           #4                  // class Foobar
      16: dup
      17: ldc           #10                 // String BAR
      19: iconst_1
      20: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      23: putstatic     #11                 // Field BAR:LFoobar;
      26: iconst_2
      27: anewarray     #4                  // class Foobar
      30: dup
      31: iconst_0
      32: getstatic     #9                  // Field FOO:LFoobar;
      35: aastore
      36: dup
      37: iconst_1
      38: getstatic     #11                 // Field BAR:LFoobar;
      41: aastore
      42: putstatic     #1                  // Field $VALUES:[LFoobar;
      45: return
}

反編譯:

final class Foobar extends java.lang.Enum<Foobar> {
    public static final Foobar FOO;
    public static final Foobar BAR;
    private static final Foobar[] $VALUES;
    public static Foobar[] values() {
        return (Foobar[]) $VALUES.clone();
    }
    public static Foobar valueOf(java.lang.String) {
        return (Foobar) java.lang.Enum.valueOf(Foobar.class, name);
    }
    private Foobar(String name, int ordinal) {
        super(name, ordinal);
    }
    static {
        FOO = new Foobar("FOO", 0);
        BAR = new Foobar("BAR", 1);
        $VALUES = new Foobar[] { FOO, BAR };
    }
}

假如 Test 還存在內部類肝集,并且我們再定義了另外的兩個枚舉類 Foo 和 Bar,那么無論是在 Test 中對 Foo 和 Bar 參數(shù)執(zhí)行switch结笨,還是在 Test 的內部類中對 Foo 和 Bar 參數(shù)執(zhí)行switch包晰,都會依賴這同一個 Test$1 類湿镀。

假如在 Test 及其內部類的源代碼中,同時對枚舉類 Foo 和 枚舉類 Bar 執(zhí)行了switch伐憾,那么在 Test$1 類中將同時存在 $SwitchMap$Foo 和 $SwitchMap$Bar 這兩個int數(shù)組勉痴。

如果兩個不同的類 A 和類 B 都對枚舉類 Foobar 執(zhí)行了switch,或是分別對枚舉類 Foo 和枚舉類 Bar 執(zhí)行了switch树肃,無論如何只要這兩個對枚舉執(zhí)行switch的類 A 和類 B 之間沒有嵌套關系蒸矛,那么就會分別生成 A$1 和 B$1 這兩個類,哪怕 A$1 和 B$1 這兩個類只有類名不同胸嘴,也仍然會重復生成雏掠。

重復地生成幾乎不變的冗余代碼,會無謂地增多類的個數(shù)劣像、增大包體積乡话、浪費類加載的時間,但是實際上編譯器這樣處理是有必要的耳奕。

這里我們需要著重理解绑青,F(xiàn)oobar、Test$1屋群、Test 這三個類之間的依賴關系:

Foobar 是在 Test 中被switch的枚舉實例所屬的類闸婴,在 Test 類被編譯時,編譯器將自動生成一個依賴于這個版本的 Test$1 類芍躏。雖然 Test$1 類依賴 Foobar 類的定義邪乍,但Test 類僅僅依賴 Foobar 類的聲明,這就意味著即使在 Test$1 類編譯完成后对竣,F(xiàn)oobar 類被修改并且重新編譯庇楞,但是并沒有重新編譯 Test 類,那么 Test$1 將不會因 Foobar 的改變而得到更新的機會柏肪,因為它只有在 Test 類被編譯時才能夠被編譯姐刁。

上面這段話也許有些饒舌,那么我還是以一段代碼作為實例:

首先編譯 Foobar:

enum Foobar {
    FOO,
    BAR;
}
javac Foobar.java

然后編譯 Test:

class Test {
    static int test(Foobar var0) {
        switch (var0) {
            case FOO:
                return 1;
            case BAR:
                return 2;
            default:
                return 0;
        }
    }
}
javac Test.java

然后修改 Foobar:

enum Foobar {
    FOOBAR,
    NIL;
}
javac Foobar.java

然后執(zhí)行以下程序:

class Main {
    public static void main(String[] args) {
        System.out.println(Test.test(Foobar.FOOBAR));
    }
}
javac Main.java
java Main

結果為 0烦味。

以上操作建議使用命令行聂使,但是使用 IDE 重現(xiàn)一遍也許印象會更為深刻:

  • 在 foobar 項目中將 Foobar 類作為 JAR 打包發(fā)布第一個版本
  • 在 test 項目中的pom.xml或者build.gradle中添加對第一個版本的 foobar 項目的依賴并將 Test 類和 Test$1 類作為 JAR 打包發(fā)布
  • 在 foobar 項目中修改 Foobar 類的代碼并作為 JAR 打包發(fā)布第二個版本
  • 在 main 項目中的pom.xml或者build.gradle中添加對 test 項目的依賴以及對第二個版本的 foobar 項目的依賴隨后運行 Main 類的 main 函數(shù)

無論使用命令行還是 IDE,在手動重現(xiàn)了這一過程后谬俄,我想應該已經(jīng)能夠大致理解我的意思柏靶,我在此還是展開解釋:

當 Test$1 類被加載的時候,將按照 Test 類編譯時所依賴的版本的 Foobar 類的枚舉常量的名字為 $SwitchMap$Foobar 數(shù)組初始化溃论,假如沒能找到這個枚舉常量屎蜓,則會拋出NoSuchFieldError并在 Test$1 中被捕獲且忽視,于是在 $SwitchMap$Foobar 數(shù)組的對應位置將要維持初始值 0钥勋,因此在 Test 類中將因這個 0 值的存在而走向default分支炬转,從而確保在項目所依賴的 SDK 之間的版本匹配錯誤的情況下辆苔,程序還可以保證自己的健壯性,避免因走入未知的分支而威脅業(yè)務安全扼劈。

參考文獻

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末驻啤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子荐吵,更是在濱河造成了極大的恐慌骑冗,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件先煎,死亡現(xiàn)場離奇詭異贼涩,居然都是意外死亡,警方通過查閱死者的電腦和手機薯蝎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進店門遥倦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人良风,你說我怎么就攤上這事谊迄∶乒” “怎么了烟央?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵,是天一觀的道長歪脏。 經(jīng)常有香客問我疑俭,道長,這世上最難降的妖魔是什么婿失? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任钞艇,我火速辦了婚禮,結果婚禮上豪硅,老公的妹妹穿的比我還像新娘哩照。我一直安慰自己,他們只是感情好懒浮,可當我...
    茶點故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布飘弧。 她就那樣靜靜地躺著,像睡著了一般砚著。 火紅的嫁衣襯著肌膚如雪次伶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天稽穆,我揣著相機與錄音冠王,去河邊找鬼。 笑死舌镶,一個胖子當著我的面吹牛柱彻,可吹牛的內容都是我干的豪娜。 我是一名探鬼主播,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼哟楷,長吁一口氣:“原來是場噩夢啊……” “哼侵歇!你這毒婦竟也來了?” 一聲冷哼從身側響起吓蘑,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤惕虑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后磨镶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溃蔫,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨居荒郊野嶺守林人離奇死亡兢孝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年庄敛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淤击。...
    茶點故事閱讀 40,973評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡脐嫂,死狀恐怖统刮,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情账千,我是刑警寧澤侥蒙,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站匀奏,受9級特大地震影響鞭衩,放射性物質發(fā)生泄漏。R本人自食惡果不足惜娃善,卻給世界環(huán)境...
    茶點故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一论衍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧聚磺,春花似錦坯台、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至矢沿,卻和暖如春滥搭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捣鲸。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工瑟匆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人栽惶。 一個月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓愁溜,卻偏偏與公主長得像疾嗅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子冕象,可洞房花燭夜當晚...
    茶點故事閱讀 45,982評論 2 361

推薦閱讀更多精彩內容

  • 今天感恩節(jié)哎代承,感謝一直在我身邊的親朋好友。感恩相遇渐扮!感恩不離不棄论悴。 中午開了第一次的黨會,身份的轉變要...
    迷月閃星情閱讀 10,576評論 0 11
  • 彩排完墓律,天已黑
    劉凱書法閱讀 4,237評論 1 3
  • 表情是什么膀估,我認為表情就是表現(xiàn)出來的情緒。表情可以傳達很多信息耻讽。高興了當然就笑了察纯,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 125,421評論 2 7