1. 程式人生 > 其它 >Java switch詳解

Java switch詳解

技術標籤:JVM基礎javajdk

在Java中switch的形式:

switch(key)
{
    case num1:
        //可以加入自己的業務邏輯
        break;

    case num2:
        //可以加入自己的業務邏輯
        break;

    default:
        //可以加入自己的業務邏輯
        break;
}

上述程式碼中switch(key)中的key可以是什麼型別?
(1)最早時,只支援int、char、byte、short這樣的整型的基本型別或對應的包裝型別Integer、Character、Byte、Short常量

(2)JDK1.5開始支援enum,原理是給列舉值進行了內部的編號,進行編號和列舉值的對映
(3)1.7開始支援String,但不允許為null,原理是藉助 hashcode( ) 來實現。
(4)此篇文章重點講述 tableswitch 和 lookupswitch

一、首先看下述程式碼

package com.Ycb.jvm;

public class SwitchTest {

    /**
     * 程式碼片段一
     *
     * @param i
     */
    public void switch1(int i) {
        int j;
        switch (i) {
            case 1:
                j = 1;
                break;
            case 2:
                j = 2;
                break;
            case 5:
                j = 3;
                break;
            default:
                break;
        }
    }

    /**
     * 程式碼片段二
     *
     * @param i
     */
    public void switch2(int i) {
        int j;
        switch (i) {
            case 1:
                j = 1;
                break;
            case 5:
                j = 2;
                break;
            case 10000:
                j = 3;
                break;
            default:
                break;
        }
    }
}

上述程式碼的邏輯幾乎一模一樣。主要就是case 中 的值不太一樣,方法switch1()中幾乎是連續的,方法switch2()中的跨度比較大,不連續。我們可以看下生成的位元組碼

方法switch1()對應的位元組碼(有省略):

public void switch1(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=2
         0: iload_1
         1: tableswitch   { // 1 to 5
                       1: 36
                       2: 41
                       3: 51
                       4: 51
                       5: 46
                 default: 51
            }
        36: iconst_1
        37: istore_2
        38: goto          51
        41: iconst_2
        42: istore_2
        43: goto          51
        46: iconst_3
        47: istore_2
        48: goto          51
        51: return

方法switch2()對應的位元組碼(有省略):

public void switch2(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=2
         0: iload_1
         1: lookupswitch  { // 3
                       1: 36
                       5: 41
                   10000: 46
                 default: 51
            }
        36: iconst_1
        37: istore_2
        38: goto          51
        41: iconst_2
        42: istore_2
        43: goto          51
        46: iconst_3
        47: istore_2
        48: goto          51
        51: return

從上面的位元組碼中可以看出方法switch1()是使用tableswitch來輔助實現switch,如下

1: tableswitch   { // 1 to 5
                       1: 36
                       2: 41
                       3: 51
                       4: 51
                       5: 46
                 default: 51
            }

(1)對於tableswitch指令,這裡high為5,low為1,表中共有high-low+1個分支項,當jvm遇到tableswitch指令時,它會檢測switch(key)中的key值是否在low~high之間,如果不是,直接執行default部分,如果在這個範圍之內,它使用key-low這個項指定的地點跳轉。可見,tableswitch的效率是非常高的。感覺這個有點類似於陣列,我們知道陣列根據下標去查詢元素的時間複雜度是O(1),效率是非常高的。

(2)我們看下方法switch1()的原始碼,case 分支只有值 1 、2 和 5,並沒有 3 和 4。但是為了便於利用陣列的連續的特性,添加了 3 和 4,所以這也就解釋了,當case中的值不連續時不宜使用tableswitch的原因,會導致很多的空間浪費,比如方法switch2()中的case 分支 的值有1 、5 和 10000,如果繼續使用tableswitch,就需要補充上 2、3、4 和 6 ~9999中間的值,而這些其實很多都是無效的值,會導致浪費很多的空間。所以針對 方法 switch2()使用lookupswitch

方法switch2()是使用lookupswitch,如下

1: lookupswitch  { // 3
                       1: 36
                       5: 41
                   10000: 46
                 default: 51
            }

switch是控制選擇的一種方式,編譯器生成程式碼時可以對這種結構進行特定的優化,從而產生效率比較高的程式碼。在java中,編譯器根據分支的情況,分別產生tableswitch和lookupswitch兩中情況,其中tableswitch適用於分支比較集中的情況,而lookupswitch適用與分支比較稀疏的情況。不過怎麼算稀疏,怎麼算集中就是編譯器的決策問題了。

JVM規範中有如下一段內容:
Compilation ofswitchstatements uses the tableswitch and lookupswitch instructions. The tableswitch instruction is used when the cases of theswitchcan be efficiently represented as indices into a table of target offsets. Thedefaulttarget of theswitchis used if the value of the expression of theswitchfalls outside the range of valid indices.Where the cases of theswitchare sparse, the table representation of the tableswitch instruction becomes inefficient in terms of space. The lookupswitch instruction may be used instead.

The Java virtual machine specifies that the table of the lookupswitch instruction must be sorted by key so that implementations may use searches more efficient than a linear scan. Even so, the lookupswitch instruction must search its keys for a match rather than simply perform a bounds check and index into a table like tableswitch. Thus, a tableswitch instruction is probably more efficient than a lookupswitch where space considerations permit a choice.

if....else 與 switch....case的初步對比分析

我們把方法switch1()的程式碼改寫下,改為使用if....else實現,如下:

public void ifSwitch(int i) {
        int j;
        if (i == 1) {
            j = 1;
        } else if (i == 2) {
            j = 2;
        } else if (i == 5) {
            j = 3;
        }
    }

我們檢視下位元組碼:

public void ifSwitch(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: iload_1
         1: iconst_1
         2: if_icmpne     10
         5: iconst_1
         6: istore_2
         7: goto          27
        10: iload_1
        11: iconst_2
        12: if_icmpne     20
        15: iconst_2
        16: istore_2
        17: goto          27
        20: iload_1
        21: iconst_5
        22: if_icmpne     27
        25: iconst_3
        26: istore_2
        27: return

我們可以看到 if....else 是使用if_icmpne指令和goto來配合實現的。當傳入的值在if分支中比較靠後或者傳入的值壓根不在if分支中,就需要執行前面的很多條if_icmpne比較,而使用switch....case 的話,如果使用 tableswitch 那直接執行O(1)的時間複雜度就可以直接執行goto跳轉,如果採用的lookupswitch,時間複雜度也小很多。if ..else走邏輯判斷時,每條if語句都獨立需要載入,都要走一遍判斷。這就是耗時的機制問題了。switch..case 根據一個值進行多路分支,只做一次計算,然後將表示式的值與每個case的值比較,進而選擇哪一個case語句塊。

但是當if 中的 條件是個範圍,比如if(key>1 && key<2)這種形式,此時switch就無能為力了,switch只能使用在等值的場景。