【JAVA】初探switch實現原理

日常編碼中,我們常常用到switch語句,在我的另外一篇文章中【JAVA】優化if else的幾種方式,也談到了可以利用switch來優化if-else結構,那麼switch底層究竟是如何實現的呢?


我們先寫幾個示例

第一個示例:case條件中的int值連續

    public int switchInt(int i) {
        int result;
        switch (i) {
            case 0:
                result = 10;
                break;
            case 1:
                result = 20;
                break;
            case 2:
                result = 30;
                break;
            default:
                result = 40;
        }
        return result;
    }

老規矩,反編譯後得到:

(首先javac Main.java,之後javap -c -p Main.class)

public class com.yang.testSwitch.Main {
  public com.yang.testSwitch.Main();
    Code:
       0: aload_0
       1: invokespecial #1         // Method java/lang/Object."<init>":()V
       4: return

  public int switchInt(int);
    Code:
       0: iload_1                       //將局部變量表下標爲1的元素,也就是傳入的i的值壓入棧頂
       1: tableswitch   { // 0 to 2     //從棧頂中彈出元素,檢查是否在[0,2]之內
                     0: 28              //如果爲0,則程序計數器跳轉到第28行
                     1: 34              //如果爲1,則程序計數器跳轉到第34行              
                     2: 40              //如果爲2,則程序計數器跳轉到第40行
               default: 46              //如果不在[0,2]內,則程序計數器跳轉到第46行
          }
      28: bipush        10              //將常量10壓入棧頂
      30: istore_2                      //將棧頂元素10存入局部變量表的第3個位置上
      31: goto          49              //跳轉到底49行
      34: bipush        20
      36: istore_2
      37: goto          49
      40: bipush        30
      42: istore_2
      43: goto          49
      46: bipush        40
      48: istore_2
      49: iload_2                       //將局部變量表中的第3個元素壓入棧中
      50: ireturn                       //彈出棧頂元素,方法返回
}

其中的tableswitch是後面會着重分析的內容,這裏先放着。

這裏有一個疑問:

爲什麼剛開始傳進來的i的值,是存到局部變量表中的第二個位置,第一個位置到底放了啥?

因爲該方法是一個實例方法,局部變量表的第一個位置總是存放着this引用。在構造方法中,已經使用aload_0將this應用存入到了局部變量表中的第一個位置上。

關於這些字節碼指令,可以參考這篇文章jvm理論-字節碼指令


第二個示例:case中的int值不連續

    public int switchInt(int i) {
        int result;
        switch (i) {
            case 0:
                result = 10;
                break;
            case 3:
                result = 20;
                break;
            case 7:
                result = 30;
                break;
            default:
                result = 40;
        }
        return result;
    }

現在改成0,3,7了,繼續觀察反編譯後得到的內容:

public class com.yang.testSwitch.Main {
  public com.yang.testSwitch.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int switchInt(int);
    Code:
       0: iload_1
       1: lookupswitch  { // 3
                     0: 36
                     3: 42
                     7: 48
               default: 54
          }
      36: bipush        10
      38: istore_2
      39: goto          57
      42: bipush        20
      44: istore_2
      45: goto          57
      48: bipush        30
      50: istore_2
      51: goto          57
      54: bipush        40
      56: istore_2
      57: iload_2
      58: ireturn
}

僅僅改動了case中的值,由連續改成了不連續,得到的字節碼指令居然發生了變化,由tableswitch變成了lookupswitch


看到這裏,我們可以總結一下

tableswitch和lookupswitch兩者的區別

tableswitch

tableswitch用來處理條件連續的情況。首先進行一次範圍檢查,檢查不通過便直接返回default。檢查通過後,會得到對應case語句的偏移量。

tableswitch使用數組結構存儲偏移量,因此利用下標可以快速定位到偏移量,時間複雜度爲O(N)

注意:這裏的“連續”可以理解爲相對連續或半連續,012連續,013可以理解爲半連續。013也會使用tableswitch,只不過裏面缺少的2和default分支會有同樣的偏移量。

lookupswitch

lookupswitch則用來處理條件不連續的情況,當條件大面積不連續時,tableswitch將會產生大量的額外空間。使用lookupswitch,會將case值進行排序,之後可以利用二分法快速查到對應的分支偏移量。

lookupswitch則是維護了一個經過key排序的(key,value)結構,查找複雜度一般爲O(logN)


第三個示例:使用String類型

在jdk1.7的時候,可以使用String類型作爲case條件了。

    public int switchString(String str) {
        int result;
        switch (str) {
            case "a":
                result = 10;
                break;
            case "c":
                result = 20;
                break;
            case "f":
                result = 30;
                break;
            default:
                result = 40;
        }
        return result;
    }

這次我們不直接看字節碼指令,僅僅觀察class文件:

public class Main {
    public Main() {
    }

    public int switchString(String var1) {
        byte var4 = -1;
        switch(var1.hashCode()) {
        case 97:
            if (var1.equals("a")) {
                var4 = 0;
            }
            break;
        case 99:
            if (var1.equals("c")) {
                var4 = 1;
            }
            break;
        case 102:
            if (var1.equals("f")) {
                var4 = 2;
            }
        }

        byte var2;
        switch(var4) {
        case 0:
            var2 = 10;
            break;
        case 1:
            var2 = 20;
            break;
        case 2:
            var2 = 30;
            break;
        default:
            var2 = 40;
        }

        return var2;
    }
}

可以看得出,主要流程爲:

  • 首先使用hashcode方法獲取到字符串的哈希值
  • 因爲case條件中的字符串的哈希值大概率不連續,所以字節碼指令一般是使用lookupswitch來保存對應的偏移量
  • 之後執行偏移量的字節碼指令,一上來會調用equals來判斷var1是否與case條件中的String相匹配(因爲可能存在兩個不同字符串的哈希值相同的情況
  • 匹配成功後,會再利用tableswitch來查詢var4代表的偏移量
  • 最後返回var2

總的來說,使用String類型來作爲case中的條件,本質上還是先轉化爲hashcode,接着查到對應的hashcode,最後再利用equals檢測內容是否相同。


第四個示例:使用枚舉類型

    public enum Animal {
        CAT, DOG
    }

    public int switchEnum(Animal animal) {
        int result;
        switch (animal) {
            case CAT:
                result = 1;
                break;
            case DOG:
                result = 2;
                break;
            default:
                result = 3;
        }
        return result;
    }

編譯該文件會得到3個文件,分別是

  1. Main.class                     這個文件是肯定有的
  2. Main$Animal.class        這個是枚舉類,也是正常的
  3. Main$1.class                  這是個啥?

進到Main$1.class發現:

import com.yang.testSwitch.Main.Animal;

// $FF: synthetic class
class Main$1 {
    static {
        try {
            $SwitchMap$com$yang$testSwitch$Main$Animal[Animal.CAT.ordinal()] = 1;
        } catch (NoSuchFieldError var2) {
        }

        try {
            $SwitchMap$com$yang$testSwitch$Main$Animal[Animal.DOG.ordinal()] = 2;
        } catch (NoSuchFieldError var1) {
        }

    }
}

使用javap -p Main$1.class後:

class com.yang.testSwitch.Main$1 {
  static final int[] $SwitchMap$com$yang$testSwitch$Main$Animal;
  static {};
}

原來這個class裏,聲明瞭一個靜態的數組,數組利用枚舉的ordinal()值作爲下標,數組中的元素依次遞增。

那這個數組到底是幹啥用的呢?

我們接着使用javap -c -p Main.class反編譯得到:

public class com.yang.testSwitch.Main {
  public com.yang.testSwitch.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int switchEnum(com.yang.testSwitch.Main$Animal);
    Code:
       0: getstatic     #2                  // Field com/yang/testSwitch/Main$1.$SwitchMap$com$yang$testSwitch$Main$Animal:[I
       3: aload_1
       4: invokevirtual #3                  // Method com/yang/testSwitch/Main$Animal.ordinal:()I
       7: iaload
       8: lookupswitch  { // 2
                     1: 36
                     2: 41
               default: 46
          }
      36: iconst_1
      37: istore_2
      38: goto          48
      41: iconst_2
      42: istore_2
      43: goto          48
      46: iconst_3
      47: istore_2
      48: iload_2
      49: ireturn
}

現在可以發現,首先是獲取到這個靜態數組,再調用枚舉的ordinal()方法獲取枚舉的值,再將這個值當作靜態數組的下標,獲取這個靜態數組中的某一個元素,然後從lookupswitch中尋找偏移量。


第五個示例:使用包裝類型

    public int switchByte(Byte i) {
        int result;
        switch (i) {
            case 1:
                result = 10;
                break;
            case 2:
                result = 20;
                break;
            case 3:
                result = 30;
                break;
            default:
                result = 40;
        }
        return result;
    }

反編譯得到:

public class com.yang.testSwitch.Main {
  public com.yang.testSwitch.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int switchByte(java.lang.Byte);
    Code:
       0: aload_1
       1: invokevirtual #2                  // Method java/lang/Byte.byteValue:()B
       4: tableswitch   { // 1 to 3
                     1: 32
                     2: 38
                     3: 44
               default: 50
          }
      32: bipush        10
      34: istore_2
      35: goto          53
      38: bipush        20
      40: istore_2
      41: goto          53
      44: bipush        30
      46: istore_2
      47: goto          53
      50: bipush        40
      52: istore_2
      53: iload_2
      54: ireturn
}

可以很清晰的看到,調用了Byte.byteValue()方法完成了對Byte的拆箱工作,之後比較byte值即可。


總結

我們用一個表格,來直觀的說明各種類型的底層操作:

         類型

                                                                    操作

       基本類型

直接比較(條件半連續,採用tableswitch,否則lookupswitch)

        String

首先獲取hashcode,之後採用equals判斷

        Enum

自動生成SwitchMap數組,下標是枚舉的ordinal(),值是從1開始遞增的整數

      包裝類型

先進行拆箱,之後比較基本類型

這裏有幾點需要注意的是:

(1)根據java虛擬機規範,java虛擬機的tableswitch和lookupswitch指令都只能支持int類型的條件值,所以switch不支持long、float、double與boolean,以及他們對應的包裝類。

(2)break語句會在字節碼層面生成一個goto語句,用來直接跳轉出switch語句塊。因此,如果有哪一次忘記寫break,那麼程序執行就會進入下一個case中,可能會造成某些匪夷所思的問題。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章