您好,登錄后才能下訂單哦!
這篇文章給大家介紹如何解決Switch報空指針異常,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
前幾天重新看 《阿里巴巴Java開發手冊》有一條這樣的規約:
出于好奇,打算研究一下!,強迫癥,沒辦法!
我們先用一個案例測試一下:
public class Test { public static void main(String[] args) { String param = null; switch (param) { case "null": System.out.println("匹配null字符串"); break; default: System.out.println("進入default"); } } }
顯而易見,如果switch傳入空值,會拋空指針!
看到這,我們先可以思考下面幾個問題:
switch 除了 String 還支持哪種類型?
為什么《阿里巴巴Java開發手冊》規定String類型參數要先進行 null 判斷?
為什么可能會拋出空指針異常?
下面開始對上面的問題進行分析
首先參考官方文檔對swtich 語句相關描述。
翻譯如下:
switch 的表達式必須是 char, byte, short, int, Character, Byte, Short, Integer, String, 或者 enum 類型,否則會發生編譯錯誤
同時switch 語句必須滿足以下條件,否則會出現編譯錯誤:
與 switch 語句關聯的每個 case 都必須和 switch 的表達式的類型一致;
如果 switch 表達式是枚舉類型,case 常量也必須是枚舉類型;
不允許同一個 switch 的兩個 case 常量的值相同;
和 switch 語句關聯的常量不能為 null ;
一個 switch 語句最多有一個 default 標簽。
翻譯如下:
switch 語句執行的時候,首先將執行 switch 的表達式。如果表達式為 null, 則會拋出 NullPointerException,整個 switch 語句的執行將被中斷。
另外從《Java虛擬機規范》這本書,我們可以學習到:
總結一下就是:
1.編譯器使用 tableswitch 和 lookupswitch 指令生成 switch 語句的編譯代碼。
2.Java 虛擬機的 tableswitch 和 lookupswitch 指令只能支持 int 類型的條件值。如果 swich 中使用其他類型的值,那么就必須轉化為 int 類型。
所以可以了解到空指針出現的根源在于:虛擬機為了實現 switch 的語法,將參數表達式轉換成 int。而這里的參數為 null, 從而造成了空指針異常。
下面對官方文檔的內容采用反匯編方式進一步分析下
不熟悉字節碼的,推薦看看美團的這篇文章:https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
下面開始硬貨!
一個例子:
public class Test { public static void main(String[] args) { String param = "月伴飛魚"; switch (param) { case "月伴飛魚1": System.out.println("月伴飛魚1"); break; case "月伴飛魚2": System.out.println("月伴飛魚2"); break; case "月伴飛魚3": System.out.println("月伴飛魚3"); break; default: System.out.println("default"); } } }
反匯編代碼得到:
Compiled from "Test.java" public class com.zhou.Test { public zhou.Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String 月伴飛魚 2: astore_1 3: aload_1 4: astore_2 5: iconst_m1 6: istore_3 7: aload_2 8: invokevirtual #3 // Method java/lang/String.hashCode:()I 11: tableswitch { // -768121881 to -768121879 -768121881: 36 -768121880: 50 -768121879: 64 default: 75 } 36: aload_2 37: ldc #4 // String 月伴飛魚1 39: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 42: ifeq 75 45: iconst_0 46: istore_3 47: goto 75 50: aload_2 51: ldc #6 // String 月伴飛魚2 53: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 56: ifeq 75 59: iconst_1 60: istore_3 61: goto 75 64: aload_2 65: ldc #7 // String 月伴飛魚3 67: invokevirtual #5 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 70: ifeq 75 73: iconst_2 74: istore_3 75: iload_3 76: tableswitch { // 0 to 2 0: 104 1: 115 2: 126 default: 137 } 104: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 107: ldc #4 // String 月伴飛魚1 109: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 112: goto 145 115: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 118: ldc #6 // String 月伴飛魚2 120: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 123: goto 145 126: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 129: ldc #7 // String 月伴飛魚3 131: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 134: goto 145 137: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 140: ldc #10 // String default 142: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 145: return }
先介紹一下下面會用到的字節碼指令
invokevirtual:調用實例方法
istore_0 將int類型值存入局部變量0
istore_1 將int類型值存入局部變量1
istore_2 將int類型值存入局部變量2
istore_3 將int類型值存入局部變量3
aload_0 從局部變量0中裝載引用類型值
aload_1 從局部變量1中裝載引用類型值
aload_2 從局部變量2中裝載引用類型值
我們繼續看匯編代碼:
先看偏移為 8 的指令,調用了參數的 hashCode() 函數來獲取字符串 "月伴飛魚" 的哈希值。
8: invokevirtual #3 // Method java/lang/String.hashCode:()I
接下來我們看偏移為 11 的指令處:
tableswitch 是跳轉引用列表, 如果值小于其中的最小值-768121881 或者大于其中的最大值-768121879,跳轉到 default 語句。
11: tableswitch { // -768121881 to -768121879 -768121881: 36 -768121880: 50 -768121879: 64 default: 75 }
其中 -768121881 為鍵,36 為對應的目標語句偏移量。
hashCode 和 tableswitch 的鍵相等,則跳轉到對應的目標偏移量,"月伴飛魚"的哈希值806505866不在最小值-768121881和最大值-768121879之間,因此跳轉到 default 對應的語句行(即偏移量為 75 的指令處執行)。
月伴飛魚的hash值計算:("月伴飛魚").hashCode();
從 36 到 75 行,根據哈希值相等跳轉到判斷是否相等的指令。
然后調用java.lang.String#equals判斷 switch 的字符串是否和對應的 case 的字符串相等。
如果相等則分別根據第幾個條件得到條件的索引,然后每個索引對應下一個指定的代碼行數。
繼續從偏移量75行往下看:
76: tableswitch { // 0 to 2 0: 104 1: 115 2: 126 default: 137 }
default 語句對應 137 行,打印 “default” 字符串,然后執行 145 行 return 命令返回。
通過 tableswitch 判斷執行哪一行打印語句。
總結就是整個流程是先計算字符串參數的哈希值,判斷哈希值的范圍,然后哈希值相等再判斷對象是否相等,然后執行對應的代碼塊。
這種先判斷 hash 值是否相等(有可能是同一個對象/兩個對象有可能相等),再通過 equals 比較 對象是否相等 的做法,在 Java 的很多 JDK 源碼中和其他框架中也非常常見的。
反匯編前言中的代碼:
public class Test { public static void main(String[] args) { String param = null; switch (param) { case "null": System.out.println("匹配null字符串"); break; default: System.out.println("進入default"); } } }
public class com.zhou.Test { public com.zhou.Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: aconst_null 1: astore_1 2: aload_1 3: astore_2 4: iconst_m1 5: istore_3 6: aload_2 7: invokevirtual #2 // Method java/lang/String.hashCode:()I 10: lookupswitch { // 1 3392903: 28 default: 39 } 28: aload_2 29: ldc #3 // String null 31: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 34: ifeq 39 37: iconst_0 38: istore_3 39: iload_3 40: lookupswitch { // 1 0: 60 default: 71 } 60: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 63: ldc #6 // String 匹配null字符串 65: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 68: goto 79 71: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 74: ldc #8 // String 進入default 76: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 79: return }
可以猜測3392903 應該是 "null" 字符串的哈希值。
10: lookupswitch { // 1 3392903: 28 default: 39 }
我們可以打印其哈希值去印證:System.out.println(("null").hashCode());
總結整體流程:
String param = null; int hashCode = param.hashCode(); if(hashCode == ("null").hashCode() && param.equals("null")){ System.out.println("null"); }else{ System.out.println("default"); }
因此空指針的原因就一目了然了:調用了 null 對象的實例方法。
關于如何解決Switch報空指針異常就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。