5年前博客寫的三目運算符空指針問題,終於被阿里開發手冊收錄了

java技術架構 發佈 2020-04-27T17:23:09+00:00

參見jls-15.25,摘要如下:Ifthe second and third operands have the same type , then that is the type of the conditional expression.If one of the seco


最近,阿里巴巴Java開發手冊發布了最新版,泰山版,這個名字起的不錯,一覽眾山小。

新版據說新增了30+規約,我還沒來得及仔細去看,不過有粉絲和我說,其中新增的一條規約,他之前在我的博客中看到過。

仔細看了下,這個問題確實我很久之前遇到過,確實曾經在博客中也記錄過。

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

這篇文章最開始發布於2015年,目前已經有1w+閱讀量了。

趁著最新的開發手冊中也提到了這個點,於是把之前的文章內容翻出來並重新整理了一下,帶大家一起回顧下這個知識點。

一、三目運算符

對於條件表達式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);

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

這和三目運算符有關。

關鍵字: