關于Java中switch關鍵字你需要知道這些

閱讀完本文我相信大家會有不少收貨的,如果遇到不懂的地方,請耐心查閱相關知識。

JDK7之后switch為什么可以支持String類型的條件判斷

記得讀大學教我們Java課程的老師曾說,switch判斷條件的數據類型只支持int和char。但是現在看來,這句話就不是那么嚴謹了,因為JDK7之后,還支持String類型的判斷條件。接下來分析一步步分析其中的原理。

示例代碼

public class TestSwitch {
    public static final java.lang.String CASE_ONE = "1";

    public static final java.lang.String CASE_TWO = "2";

    public static final java.lang.String CASE_THERE = "3";

    public static final java.lang.String CASE_FOUR = "4";

    public void testSwitch(String key) {
        switch (key) {
        case CASE_ONE:
            break;
        case CASE_TWO:
            break;
        case CASE_THERE:
            break;
        case CASE_FOUR:
            break;
        default:
            break;
        }
    }
}

接著通過以下命令,將該Java文件轉成Class文件

javac TestSwtich.java

然后通過以下命令,將編譯后的Class文件進行反編譯

javap TesTSwitch.class

(注意:以上javac和javap命令是JDK工具提供的,如有不了解的可以通過javac -help和javap -help進行了解)

得到如下匯編代碼。

public class TestSwitch {
  public static final java.lang.String CASE_ONE;

  public static final java.lang.String CASE_TWO;

  public static final java.lang.String CASE_THERE;

  public static final java.lang.String CASE_FOUR;

  public TestSwitch();

    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  //這里開始是分析的重點
  public void testSwitch(java.lang.String);

    Code:
//將方法參數key加載進操作數棧
       0: aload_1
//接著將該方法參數key存儲到局部變量表
       1: astore_2
//將int常量-1壓入操作數棧中
       2: iconst_m1
//接著將剛壓入棧中的常量-1存儲到局部變量表中
       3: istore_3
//將局部變量表中的存儲的方法參數key加載到操作數棧頂
       4: aload_2
//這一步是關鍵,接著虛擬機會調用String的hashCode方法,
//即對示例源碼中key值進行hashCode,這樣做的目的就是轉成對int類型的判斷
       5: invokevirtual #2                  // Method java/lang/String.hashCode:()I
//taleswitch是Java虛擬機對應Java源碼中switch關鍵字的指令                                 
       8: tableswitch   { // 49 to 52       
//49對應著常量字符串“1”的hashCode值,也是字符‘1’的ASCII值,
//大家可以看下String類hasCode的源碼就會知道為什么相等了。
//如果字符串key的hashCode值等于常量字符串“1”的hashCode值,
//則跳轉到行號為40的地方繼續執行指令
                    49: 40                  
                    50: 54
                    51: 68
                    52: 82
               default: 93
          }
//將局部變量表中的存儲的方法參數key加載到操作數棧頂        
      40: aload_2                          
// 將常量池中的常量字符串“1”壓入棧中
      41: ldc           #3                
//接著調用String的equals方法將常量字符串“1”和key進行比較,接著講返回值壓入棧頂
//雖然equals方法的返回值是布爾類型,但是Java虛擬機會將布爾類型窄化成int型。
      43: invokevirtual #4   // Method java/lang/String.equals:(Ljava/lang/Object;)Z
                                           
 //從棧頂中彈出int型數據,如果為0則跳轉到行號為93的地方進行執行,0代表false
      46: ifeq          93                
  //將int型常量0壓入棧中 
//為什么會把0壓入棧中?因為虛擬機會將第一個case情況默認賦值為0,后面的case情況依次+1
      49: iconst_0
  //將int型常量0存儲到局部變量表中                        
      50: istore_3              
  //接著跳轉到行號為93的地方繼續執行         
      51: goto          93               
      54: aload_2
      55: ldc           #5                  // String 2
      57: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      60: ifeq          93
      63: iconst_1
      64: istore_3
      65: goto          93
      68: aload_2
      69: ldc           #6                  // String 3
      71: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      74: ifeq          93
      77: iconst_2
      78: istore_3
      79: goto          93
      82: aload_2
      83: ldc           #7                  // String 4
      85: invokevirtual #4                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      88: ifeq          93
      91: iconst_3
      92: istore_3
//以上每種case情況,最終會跳轉到該行指令執行
//上面分析中,如果case情況為字符串“1”,則會保存一個int常量0,
//這里0也就代表了case為“1”的情況。
//這句指令會把先前存儲在局部變量表中的int值加載到棧頂
      93: iload_3                          
      94: tableswitch   { // 0 to 3         
//如果等于0,則跳轉到124行指令處執行
                     0: 124                
                     1: 127
                     2: 130
                     3: 133
               default: 136
          }
     124: goto          136
     127: goto          136
     130: goto          136
     133: goto          136
     136: return
}

以上代碼中,我分析了"case CASE_ONE:"情況,雖然我們Java代碼中只用了一個switch關鍵字,但是編譯器生成的字節碼卻用了兩個tableswitch指令。第一條tableswitch指令是根據字符串key哈希之后的int值進行調整判斷,跳轉到相應的行號之后,接著調用equals方法進行字符串內容比較,如果內容相等,會將每種case情況用一個int值記錄,從0開始依次加1。第二條tableswitch指令會根據每種case情況所對應的int值進行判斷,最終轉化為switch的判斷條件為int類型的情況。
由此可見,用String類型作為判斷條件,編譯器編譯后的指令也會相應的增加。因此建議,能夠用int值作為判斷條件的就用int值吧。

如果對Java虛擬機指令不了解的,請耐心翻閱相關書籍或查閱相關資料。我相信你也會有不少收貨的。

如何寫出高效的switch代碼

看到這個標題,不要驚訝。我們邊寫示例邊分析原理。

示例代碼1

public class TestSwitch {

    public static final int CASE_ONE = 1;

    public static final int CASE_TWO = 2;

    public static final int CASE_THERE = 3;

    public void testSwitch(int key) {
        switch (key) {
        case CASE_ONE:
            break;
        case CASE_TWO:
            break;
        case CASE_THERE:
            break;
        default:
            break;
        }
    }
}

反編譯后得到:

public class TestSwitch {
  public static final int CASE_ONE;

  public static final int CASE_TWO;

  public static final int CASE_THERE;

  public TestSwitch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void testSwitch(int);
    Code:
       0: iload_1
       1: tableswitch   { // 1 to 3
                     1: 28
                     2: 31
                     3: 34
               default: 37
          }
      28: goto          37
      31: goto          37
      34: goto          37
      37: return
}

示例代碼2

“示例代碼2”在“示例代碼1”的基礎上,僅僅修改了最后一個case情況的判斷常量數的值。從“3”變成“5"。

public class TestSwitch {

    public static final int CASE_ONE = 1;

    public static final int CASE_TWO = 2;

    public static final int CASE_FIVE = 5;

    public void testSwitch(int key) {
        switch (key) {
        case CASE_ONE:
            break;
        case CASE_TWO:
            break;
        case CASE_FIVE:
            break;
        default:
            break;
        }
    }
}

反編譯后得到

public class TestSwitch {
  public static final int CASE_ONE;

  public static final int CASE_TWO;

  public static final int CASE_FIVE;

  public TestSwitch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void testSwitch(int);
    Code:
       0: iload_1
       1: tableswitch   { // 1 to 5
//“示例代碼2”相對“示例代碼1”來說僅僅改變了最后一個case的判斷常量數的值。
//但是會導致所有case判斷常量數的值不連續了?!笆纠a1”是“1,2,3”,這個三個數是連續的。
//但是本示例中變成了“1,2,5”,這個三個數就不連續了。tableswitch這個指令比較“聰明”的。
//如果判斷數值是不連續的,且又不是那么離散,那么它會自動把中間缺的判斷常量數給補上。
//例如下面代碼,判斷條件3和4是編譯器幫我們補上的。
//補上后有什么好處呢?補上后,“1,2,3,4,5”這些判斷條件值就是一個連續值的整型數組,利于判斷的直接跳轉。
//比如我們傳入的判斷條件數值是3,即switch(key)中key值為3,
//那么虛擬機會首先判斷3是否在1-5之間,
//如果在則取目標值1(即下面“1:36”的這行代碼)為參照,
//接著直接跳轉到數組中(3-1)項(注意:數組是從0開始),即“3:45”代碼出。
                     1: 36
                     2: 39
                     3: 45
                     4: 45
                     5: 42
               default: 45
          }
      36: goto          45
      39: goto          45
      42: goto          45
      45: return
}

示例代碼3

public class TestSwitch {

    public static final int CASE_ONE = 1;

    public static final int CASE_TWO = 2;

    public static final int CASE_TEN = 10;

    public void testSwitch(int key) {
        switch (key) {
        case CASE_ONE:
            break;
        case CASE_TWO:
            break;
        case CASE_TEN:
            break;
        default:
            break;
        }
    }
}

反編譯后得到

public class TestSwitch {
  public static final int CASE_ONE;

  public static final int CASE_TWO;

  public static final int CASE_TEN;

  public TestSwitch();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void testSwitch(int);
    Code:
       0: iload_1
       1: lookupswitch  { // 3
//這里我們將最后一個判斷條件的數值改為10后,
//這里出現的不是tableswitch指令,而是lookupswitch指令了。
//原因是,case中所有的判斷條件的數值比較離散了,如果系統繼續幫我們補其剩余判斷數的話(即從3到9),
//那么會浪費不少內存空間。因此這里改用lookupswitch指令,那么該指令是如何執行呢?
//其實很簡單就是一步一步的比較下去,知道碰到條件滿足的。
                     1: 36
                     2: 39
                    10: 42
               default: 45
          }
      36: goto          45
      39: goto          45
      42: goto          45
      45: return
}

這三種情況我只改變了最后一個case的判斷常量數的值,但是得到的反編譯代碼卻有所不同。
對于編譯器來說,switch的case判斷條件值,有三種情況。
1.判斷值都是連續數字。
2.判斷數字不連續但也不很離散。
3.判斷數字比較離散。
以上第一種和第二種情況是使用tableswitch指令,第三種情況使用lookupswitch指令。
這里大家可能會對第二種情況有些許疑問,就是如何判斷不連續但也不很離散,這里虛擬機有一套判斷規則,會根據switch語句的case個數和case的所有判斷常量數值的離散情況而定。
簡而言之,tableswitch指令是以空間換時間來提供效率,而lookupswitch會“犧牲”效率來換取空間。但是我們不需要考慮這些,因為聰明的編譯器會幫我們搞定。

使用switch關鍵字的建議。

switch是我們經常打交道的關鍵字,但是在寫判斷條件的時候我們不應該很隨意設置。
比如

public class TestSwitch {

    public static final int DO_SWIM = 1;

    public static final int DO_EAT = 2;

    public static final int DO_DRINK = 3;
        ……
    public void testSwitch(int key) {
        switch (key) {
        case DO_SWIM:
            break;
        case DO_EAT:
            break;
        case DO_DRINK:
            break;
                ……
        default:
            break;
        }
    }
}

DO_SWIM、DO_EAT、DO_DRINK……,我們分別代表三種不用行為,如果分別設置為1、2、3……這種連續數據。那么這種代碼就是相當完美的。但是有時候有些程序員比較“另類”,可能會設成1,3,5,7……這種純"奇"數據?;蛘?0,100,1000,10000……這種霸氣數據。后兩種情況的數據要么是會造成空間浪費,要么是會影響執行效率。因此,我們在寫switch指令時,如果情況允許,最好將case的判斷數據設置為連續的,同時對減少apk體積也有那么點幫助。

寫在最后

小小的代碼背后都可能會蘊含高深的原理,保持一顆求知的心,我們才能不斷進步,共勉之!當然,如果文章有何錯誤或寫的不明白之處,歡迎大家指出,非常感謝閱讀!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容