三目運算符報的空指針問題,自動拆箱導致空指針異常問題

仔細看了下,這個問題確實我很久之前遇到過,所以這次在此做個記錄

最初遇到這個問題的是我的同事,他在代碼中使用了三目運算符,代碼在線上運行的時候發生了NPE,經過排查,發現原來是三目運算符和自動拆裝箱之間有一定的關係,導致了空指針。 

一、三目運算符

對於條件表達式b?x:y,先計算條件b,然後進行判斷。如果b的值爲true,計算x的值,運算結果爲x的值;否則,計算y的值,運算結果爲y的值。一個條件表達式從不會既計算x,又計算y。條件運算符是右結合的,也就是說,從右向左分組計算。例如,a?b:c?d:e將按a?b:(c?d:e)執行。

二、自動裝箱與自動拆箱

基本數據類型的自動裝箱(autoboxing)、拆箱(unboxing)是自J2SE 5.0開始提供的功能。

一般我們要創建一個類的對象實例的時候,我們會這樣:Class a = new Class(parameters);

當我們創建一個Integer對象時,卻可以這樣:Integer i = 100;(注意:和 int i = 100;是有區別的 )

實際上,執行上面那句代碼的時候,系統爲我們執行了:Integer i = Integer.valueOf(100);

這裏暫且不討論這個原理是怎麼實現的(何時拆箱、何時裝箱),也略過普通數據類型和對象類型的區別。

我們可以理解爲,當我們自己寫的代碼符合裝(拆)箱規範的時候,編譯器就會自動幫我們拆(裝)箱。

那麼,這種不被程序員控制的自動拆(裝)箱會不會存在什麼問題呢?

三、問題回顧

首先,通過你已有的經驗看一下下面這段代碼:

Map<String,Boolean> map =  new HashMap<String, Boolean>();

Boolean b = (map!=null ? map.get("test") : false);

以上這段代碼,是我們在不注意的情況下有可能經常會寫的一類代碼(在很多時候我們都愛使用三目運算符)。當然,這段代碼是存在問題的,執行該代碼,會報NPE.

Exception in thread "main" java.lang.NullPointerException

首先可以明確的是,既然報了空指針,那麼一定是有些地方調用了一個null的對象的某些方法。

在這短短的兩行代碼中,看上去只有一處方法調用map.get("test"),但是我們也都是知道,map已經事先初始化過了,不會是Null,那麼到底是哪裏有空指針呢。

我們接下來反編譯一下該代碼。看看我們寫的代碼在經過編譯器處理之後變成了什麼樣。

反編譯後代碼如下:

HashMap hashmap = new HashMap();

Boolean boolean1 = Boolean.valueOf(hashmap == null ? false : ((Boolean)hashmap.get("test")).booleanValue());

看完這段反編譯之後的代碼之後,經過分析我們大概可以知道問題出在哪裏。

((Boolean)hashmap.get("test")).booleanValue()的執行過程及結果如下:

hashmap.get("test")->null;

(Boolean)null->null;

null.booleanValue()->報錯

好,問題終於定位到了。那麼接下來看看如何解決該問題以及爲什麼會出現這種問題。

四、原理分析

通過查看反編譯之後的代碼,我們準確的定位到了問題,分析之後我們可以得出這樣的結論:NPE的原因應該是三目運算符和自動拆箱導致了空指針異常。

根據規定,三目運算符的第二、第三位操作數的返回值類型應該是一樣的,這樣才能當把一個三目運算符的結果賦值給一個變量。

如:Person i = a>b ?i1:i2; ,就要求i1和i2的類型都必須是Person才行。

因爲Java中存在一種特殊的情況,那就是基本數據類型和包裝數據類型可以通過自動拆裝箱的方式互相轉換。即可以定義int i = new Integer(10);也可以定義Integer i= 10;

那如果,三目運算符的第二位和第三位的操作數的類型分別是基本數據類型和包裝類型對象時,就需要有一方需要進行自動拆裝箱。

那到底如何做的呢,根據三目運算符的語法規範。參見jls-15.25,摘要如下:

If the second and third operands have the same type (which may be the null type), then that is the type of the conditional expression.

If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.

If one of the second and third operands is of the null type and the type of the other is a reference type, then the type of the conditional expression is that reference type.

簡單的來說就是:當第二,第三位操作數分別爲基本類型和對象時,其中的對象就會拆箱爲基本類型進行操作。

所以,結果就是:由於使用了三目運算符,並且第二、第三位操作數分別是基本類型和對象。所以對對象進行拆箱操作,由於該對象爲null,所以在拆箱過程中調用null.booleanValue()的時候就報了NPE。

五、問題解決

如果代碼這麼寫,就不會報錯:

Map<String,Boolean> map =  new HashMap<String, Boolean>();

Boolean b = (map!=null ? map.get("test") : Boolean.FALSE);

就是保證了三目運算符的第二第三位操作數都爲對象類型。

這和三目運算符有關。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章