深刻理解Java的switch...case...語句

switch...case...中條件表達式的演進

  • 最先時,只支持int、char、byte、short這樣的整型的基本類型或對應的包裝類型Integer、Character、Byte、Short常量
  • JDK1.5開始支持enum,原理是給枚舉值進行了內部的編號,進行編號和枚舉值的映射
  • 1.7開始支持String,但不容許爲null。(緣由能夠看後文)

case表達式僅限字面值常量嗎?

case表達式既能夠用字面值常量,也能夠用final修飾且初始化過的變量。例如如下代碼可正常編譯並執行:html

public static int test(int i) {
        final int j = 2;
        int result;
        switch (i) {
            case 0:
                result = 0;
                break;
            case j:
                result = 1;
                break;
            case 10:
                result = 4;
                break;
            default:
                result = -1;
        }
        return result;
    }

可是沒有初始化就不行,好比下面的代碼就沒法經過編譯java

public class SwitchTest {

    private final int caseJ;

    public int test(int i) {
        int result;
        switch (i) {
            case 0:
                result = 0;
                break;
            case caseJ:
                result = 1;
                break;
            case 10:
                result = 4;
                break;
            default:
                result = -1;
        }
        return result;
    }

    SwitchTest(int caseJ) {
        this.caseJ = caseJ;
    }

    public static void main(String[] args) {
        SwitchTest testJ = new SwitchTest(1);
        System.out.print(testJ.test(2));
    }
}

lookupswitch和tableswitch

下面兩種幾乎同樣的代碼,會編譯出截然不同的字節碼。數組

lookupswitch

public static int test(int i) {

        int result;
        switch (i) {
            case 0:
                result = 0;
                break;
            case 2:
                result = 1;
                break;
            case 10:
                result = 4;
                break;
            default:
                result = -1;
        }
        return result;
    }

對應字節碼安全

public static int test(int);
    Code:
       0: iload_0
       1: lookupswitch  { // 3
                     0: 36
                     2: 41
                    10: 46
               default: 51
          }
      36: iconst_0
      37: istore_1
      38: goto          53
      41: iconst_1
      42: istore_1
      43: goto          53
      46: iconst_4
      47: istore_1
      48: goto          53
      51: iconst_m1
      52: istore_1
      53: iload_1
      54: ireturn

tableswitch

public static int test(int i) {

        int result;
        switch (i) {
            case 0:
                result = 0;
                break;
            case 2:
                result = 1;
                break;
            case 4:
                result = 4;
                break;
            default:
                result = -1;
        }
        return result;
    }
public static int test(int);
    Code:
       0: iload_0
       1: tableswitch   { // 0 to 4
                     0: 36
                     1: 51
                     2: 41
                     3: 51
                     4: 46
               default: 51
          }
      36: iconst_0
      37: istore_1
      38: goto          53
      41: iconst_1
      42: istore_1
      43: goto          53
      46: iconst_4
      47: istore_1
      48: goto          53
      51: iconst_m1
      52: istore_1
      53: iload_1
      54: ireturn

兩種字節碼,最大的區別是執行了不一樣的指令:lookupswitch和tableswitch。dom

兩種switch區別

  • tableswitch使用了一個數組,經過下標能夠直接定位到要跳轉的行。可是在生成字節碼時,有的行可能在源碼中並不存在。經過這種方式能夠得到O(1)的時間複雜度。
  • lookupswitch維護了一個key-value的關係,經過逐個比較索引來查找匹配的待跳轉的行數。而查找最好的性能是O(log n),如二分查找。
    可見,經過用冗餘的機器碼,tableswitch換取了更好的性能。

可是,在分支比較少的狀況下,O(log n)其實並不大。n=2時,log n 約爲2.8;即便n=100, log n 約爲 6.6,與1仍未達到1個數量級的差距。jvm

什麼時候生成tableswitch?什麼時候生成lookupswitch?

在JDK1.8環境下,經過檢索langtools這個包,能夠在langtools/src/share/classes/com/sun/tools/javac/jvm/Gen.java看到如下代碼:ide

long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
     nlabels > 0 && table_space_cost + 3 * table_time_cost <= lookup_space_cost + 3 * lookup_time_cost
                ?
                tableswitch : lookupswitch;

這段代碼的上下文:性能

  • hi和lo分別表明值的上下限,是經過遍歷switch...case...每一個分支獲取的。
  • nlabels表示switch...case...的分支個數

能夠看出,決策的條件綜合考慮了時間複雜度(table_time_cost/lookup_time_cost)和空間複雜度(table_space_cost/lookup_space_cost),而且時間複雜度的權重是空間複雜度的3倍。學習

存疑點:優化

  • 各類幻數沒有解釋取值的緣由,好比四、3,應該和具體細節實現有關。
  • lookupswitch的時間複雜度使用的是nlabels而沒有取log n。此處能夠看作是近似計算。

switch...case...優於if...else...嗎?

通常來講,更多的限制能帶來更好的性能。
從上文能夠看出,不管是tableswitch仍是lookupswitch,都有對隨機查找的優化,而if...else...是沒有的,能夠看下面的源碼和字節碼。

public static int test2(int i) {

        int result;
        if(i == 0) {
            result = 0;
        } else if(i == 1) {
            result = 1;
        } else if(i == 4) {
            result = 4;
        } else {
            result = -1;
        }
        return result;
    }
public static int test2(int);
    Code:
       0: iload_0
       1: ifne          9
       4: iconst_0
       5: istore_1
       6: goto          31
       9: iload_0
      10: iconst_1
      11: if_icmpne     19
      14: iconst_1
      15: istore_1
      16: goto          31
      19: iload_0
      20: iconst_4
      21: if_icmpne     29
      24: iconst_4
      25: istore_1
      26: goto          31
      29: iconst_m1
      30: istore_1
      31: iload_1
      32: ireturn

字符串常量的case表達式及字節碼

舉例以下,這段源碼有兩個特色:

  1. case "ghi"分支裏是沒有賦值代碼
  2. case "test"分支和case "test2"分支相同
public static int testString(String str) {

        int result = -4;
        switch (str) {
            case "abc":
                result = 0;
                break;
            case "def":
                result = 1;
                break;
            case "ghi":
                break;
            case "test":
            case "test2":
                result = 1;
                break;
            default:
                result = -1;
        }
        return result;
    }

對應字節碼

public static int testString(java.lang.String);
    Code:
       0: bipush        -4
       2: istore_1
       3: aload_0
       4: astore_2
       5: iconst_m1
       6: istore_3
       7: aload_2
       8: invokevirtual #2                  // Method java/lang/String.hashCode:()I
      11: lookupswitch  { // 5
                 96354: 60
                 99333: 74
                102312: 88
               3556498: 102
             110251488: 116
               default: 127
          }
      60: aload_2
      61: ldc           #3                  // String abc
      63: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      66: ifeq          127
      69: iconst_0
      70: istore_3
      71: goto          127
      74: aload_2
      75: ldc           #5                  // String def
      77: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      80: ifeq          127
      83: iconst_1
      84: istore_3
      85: goto          127
      88: aload_2
      89: ldc           #6                  // String ghi
      91: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      94: ifeq          127
      97: iconst_2
      98: istore_3
      99: goto          127
     102: aload_2
     103: ldc           #7                  // String test
     105: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
     108: ifeq          127
     111: iconst_3
     112: istore_3
     113: goto          127
     116: aload_2
     117: ldc           #8                  // String test2
     119: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
     122: ifeq          127
     125: iconst_4
     126: istore_3
     127: iload_3
     128: tableswitch   { // 0 to 4
                     0: 164
                     1: 169
                     2: 174
                     3: 177
                     4: 177
               default: 182
          }
     164: iconst_0
     165: istore_1
     166: goto          184
     169: iconst_1
     170: istore_1
     171: goto          184
     174: goto          184
     177: iconst_1
     178: istore_1
     179: goto          184
     182: iconst_m1
     183: istore_1
     184: iload_1
     185: ireturn

能夠看到與整型常量的不一樣:

  1. String常量判等,先計算hashCode,在lookupswitch分支中再比較是否真正相等。這也是不支持null的緣由,此時hashCode沒法計算。
  2. lookupswitch分支中,會給每一個分支分配一個新下標值,做爲後面的tableswitch的索引。源碼中的分支語句統一在tableswitch中對應分支執行。

爲何要再生成一段tableswitch?從字節碼來看,兩個平行的分支("test"和"test2"),雖然沒有在tableswitch中用同一個數組下標,可是使用了同一個跳轉行177,在這種狀況下減小了字節碼冗餘。

枚舉的case表達式及字節碼

樣例代碼以下

public static int testEnum(StatusEnum statusEnum) {

        int result;
        switch (statusEnum) {
            case INIT:
                result = 0;
                break;
            case FINISH:
                result = 1;
                break;
            default:
                result = -1;
        }
        return result;
    }

對應字節碼

public static int testEnum(com.example.StatusEnum);
    Code:
       0: getstatic     #9                  // Field com/example/SwitchTest$1.$SwitchMap$com$example$core$service$domain$enums$StatusEnum:[I
       3: aload_0
       4: invokevirtual #10                 // Method com/example/core/service/domain/enums/StatusEnum.ordinal:()I
       7: iaload
       8: lookupswitch  { // 2
                     1: 36
                     2: 41
               default: 46
          }
      36: iconst_0
      37: istore_1
      38: goto          48
      41: iconst_1
      42: istore_1
      43: goto          48
      46: iconst_m1
      47: istore_1
      48: iload_1
      49: ireturn

能夠看到,使用了枚舉的ordinal方法肯定序號。

其餘

經過查看字節碼,能夠發現源碼的break關鍵字,對應的是字節碼goto到具體行的語句。 若是不用break,那麼對應的字節碼就會「滑落」到下一行語句,繼續執行。

附1——idea查看字節碼方法

Mac下preference->Tools->External Tools,點擊+,按以下頁面配置便可。

Windows下須要將上圖填入的javap改成javap.exe。

注意:每次查看字節碼前,要確保對應類被從新編譯,才能看到最新版。

附2——JDK7或8下,switch...case...使用字符串常量編譯報錯解決方式

這種狀況的真實緣由是,JDK設置不一致,IDE沒有徹底使用預期的編譯器版本。
在IDEA裏能夠這樣解決:
Project Settings -> Project 設置項目語言
若是仍未解決,檢查
File -> Project Structure -> Modules, 查看全部模塊是否都是預期的等級。
還有一處也能夠看下File -> Settings -> Compiler -> Java Compiler. 這裏能夠設置項目及模塊的編譯器版本。

備註

文中全部log n均爲以2爲底n的對數。
本文的寫做契機是參加公司的XX安全學習,提到了switch...case...和if...else...的性能有差別,所以花了一天研究了一番。

參考文檔

經過字節碼分析java中的switch語句
Difference between JVM's LookupSwitch and TableSwitch?
IntelliJ switch statement using Strings error: use -source 7
Intellij idea快速查看Java類字節碼

相關文章
相關標籤/搜索