您好,登錄后才能下訂單哦!
這篇文章給大家介紹如何解決Switch報(bào)空指針異常,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。
前幾天重新看 《阿里巴巴Java開(kāi)發(fā)手冊(cè)》有一條這樣的規(guī)約:
出于好奇,打算研究一下!,強(qiáng)迫癥,沒(méi)辦法!
我們先用一個(gè)案例測(cè)試一下:
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("進(jìn)入default"); } } }
顯而易見(jiàn),如果switch傳入空值,會(huì)拋空指針!
看到這,我們先可以思考下面幾個(gè)問(wèn)題:
switch 除了 String 還支持哪種類(lèi)型?
為什么《阿里巴巴Java開(kāi)發(fā)手冊(cè)》規(guī)定String類(lèi)型參數(shù)要先進(jìn)行 null 判斷?
為什么可能會(huì)拋出空指針異常?
下面開(kāi)始對(duì)上面的問(wèn)題進(jìn)行分析
首先參考官方文檔對(duì)swtich 語(yǔ)句相關(guān)描述。
翻譯如下:
switch 的表達(dá)式必須是 char, byte, short, int, Character, Byte, Short, Integer, String, 或者 enum 類(lèi)型,否則會(huì)發(fā)生編譯錯(cuò)誤
同時(shí)switch 語(yǔ)句必須滿足以下條件,否則會(huì)出現(xiàn)編譯錯(cuò)誤:
與 switch 語(yǔ)句關(guān)聯(lián)的每個(gè) case 都必須和 switch 的表達(dá)式的類(lèi)型一致;
如果 switch 表達(dá)式是枚舉類(lèi)型,case 常量也必須是枚舉類(lèi)型;
不允許同一個(gè) switch 的兩個(gè) case 常量的值相同;
和 switch 語(yǔ)句關(guān)聯(lián)的常量不能為 null ;
一個(gè) switch 語(yǔ)句最多有一個(gè) default 標(biāo)簽。
翻譯如下:
switch 語(yǔ)句執(zhí)行的時(shí)候,首先將執(zhí)行 switch 的表達(dá)式。如果表達(dá)式為 null, 則會(huì)拋出 NullPointerException,整個(gè) switch 語(yǔ)句的執(zhí)行將被中斷。
另外從《Java虛擬機(jī)規(guī)范》這本書(shū),我們可以學(xué)習(xí)到:
總結(jié)一下就是:
1.編譯器使用 tableswitch 和 lookupswitch 指令生成 switch 語(yǔ)句的編譯代碼。
2.Java 虛擬機(jī)的 tableswitch 和 lookupswitch 指令只能支持 int 類(lèi)型的條件值。如果 swich 中使用其他類(lèi)型的值,那么就必須轉(zhuǎn)化為 int 類(lèi)型。
所以可以了解到空指針出現(xiàn)的根源在于:虛擬機(jī)為了實(shí)現(xiàn) switch 的語(yǔ)法,將參數(shù)表達(dá)式轉(zhuǎn)換成 int。而這里的參數(shù)為 null, 從而造成了空指針異常。
下面對(duì)官方文檔的內(nèi)容采用反匯編方式進(jìn)一步分析下
不熟悉字節(jié)碼的,推薦看看美團(tuán)的這篇文章:https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
下面開(kāi)始硬貨!
一個(gè)例子:
public class Test { public static void main(String[] args) { String param = "月伴飛魚(yú)"; switch (param) { case "月伴飛魚(yú)1": System.out.println("月伴飛魚(yú)1"); break; case "月伴飛魚(yú)2": System.out.println("月伴飛魚(yú)2"); break; case "月伴飛魚(yú)3": System.out.println("月伴飛魚(yú)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 月伴飛魚(yú) 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 月伴飛魚(yú)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 月伴飛魚(yú)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 月伴飛魚(yú)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 月伴飛魚(yú)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 月伴飛魚(yú)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 月伴飛魚(yú)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 }
先介紹一下下面會(huì)用到的字節(jié)碼指令
invokevirtual:調(diào)用實(shí)例方法
istore_0 將int類(lèi)型值存入局部變量0
istore_1 將int類(lèi)型值存入局部變量1
istore_2 將int類(lèi)型值存入局部變量2
istore_3 將int類(lèi)型值存入局部變量3
aload_0 從局部變量0中裝載引用類(lèi)型值
aload_1 從局部變量1中裝載引用類(lèi)型值
aload_2 從局部變量2中裝載引用類(lèi)型值
我們繼續(xù)看匯編代碼:
先看偏移為 8 的指令,調(diào)用了參數(shù)的 hashCode() 函數(shù)來(lái)獲取字符串 "月伴飛魚(yú)" 的哈希值。
8: invokevirtual #3 // Method java/lang/String.hashCode:()I
接下來(lái)我們看偏移為 11 的指令處:
tableswitch 是跳轉(zhuǎn)引用列表, 如果值小于其中的最小值-768121881 或者大于其中的最大值-768121879,跳轉(zhuǎn)到 default 語(yǔ)句。
11: tableswitch { // -768121881 to -768121879 -768121881: 36 -768121880: 50 -768121879: 64 default: 75 }
其中 -768121881 為鍵,36 為對(duì)應(yīng)的目標(biāo)語(yǔ)句偏移量。
hashCode 和 tableswitch 的鍵相等,則跳轉(zhuǎn)到對(duì)應(yīng)的目標(biāo)偏移量,"月伴飛魚(yú)"的哈希值806505866不在最小值-768121881和最大值-768121879之間,因此跳轉(zhuǎn)到 default 對(duì)應(yīng)的語(yǔ)句行(即偏移量為 75 的指令處執(zhí)行)。
月伴飛魚(yú)的hash值計(jì)算:("月伴飛魚(yú)").hashCode();
從 36 到 75 行,根據(jù)哈希值相等跳轉(zhuǎn)到判斷是否相等的指令。
然后調(diào)用java.lang.String#equals判斷 switch 的字符串是否和對(duì)應(yīng)的 case 的字符串相等。
如果相等則分別根據(jù)第幾個(gè)條件得到條件的索引,然后每個(gè)索引對(duì)應(yīng)下一個(gè)指定的代碼行數(shù)。
繼續(xù)從偏移量75行往下看:
76: tableswitch { // 0 to 2 0: 104 1: 115 2: 126 default: 137 }
default 語(yǔ)句對(duì)應(yīng) 137 行,打印 “default” 字符串,然后執(zhí)行 145 行 return 命令返回。
通過(guò) tableswitch 判斷執(zhí)行哪一行打印語(yǔ)句。
總結(jié)就是整個(gè)流程是先計(jì)算字符串參數(shù)的哈希值,判斷哈希值的范圍,然后哈希值相等再判斷對(duì)象是否相等,然后執(zhí)行對(duì)應(yīng)的代碼塊。
這種先判斷 hash 值是否相等(有可能是同一個(gè)對(duì)象/兩個(gè)對(duì)象有可能相等),再通過(guò) equals 比較 對(duì)象是否相等 的做法,在 Java 的很多 JDK 源碼中和其他框架中也非常常見(jiàn)的。
反匯編前言中的代碼:
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("進(jìn)入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 進(jìn)入default 76: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 79: return }
可以猜測(cè)3392903 應(yīng)該是 "null" 字符串的哈希值。
10: lookupswitch { // 1 3392903: 28 default: 39 }
我們可以打印其哈希值去印證:System.out.println(("null").hashCode());
總結(jié)整體流程:
String param = null; int hashCode = param.hashCode(); if(hashCode == ("null").hashCode() && param.equals("null")){ System.out.println("null"); }else{ System.out.println("default"); }
因此空指針的原因就一目了然了:調(diào)用了 null 對(duì)象的實(shí)例方法。
關(guān)于如何解決Switch報(bào)空指針異常就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。