1. 程式人生 > 其它 >淺析Java中三目運算子可能產生的坑

淺析Java中三目運算子可能產生的坑

  三目運算子是我們經常在程式碼中使用的,a= (b==null?0:1);這樣一行程式碼可以代替一個if-else,可以使程式碼變得清爽易讀。

  但是,三目運算子也是有一定的語言規範的。在運用不恰當的時候會導致意想不到的問題。

  前段時間遇到(一個由於使用三目運算子導致的問題,其實是因為有三目運算子和自動拆箱同時使用(雖然自動拆箱不是我主動用的)。

一、三目運算子

  三元元素的格式:【條件控制語句】 ? 【表示式1】 : 【表示式2】。

1、對於條件表示式b?x:y,先計算條件b,然後進行判斷。如果b的值為true,計算x的值,運算結果為x的值;否則,計算y的值,運算結果為y的值。

2、一個條件表示式從不會既計算x,又計算y。

3、條件運算子是右結合的,也就是說,從右向左分組計算。例如,a?b:c?d:e將按a?b:(c?d:e)執行。

  優點:一些簡單的邏輯判斷三元運算子可以簡化程式碼,去除多餘的 if-else 語句。

  缺點:三元運算子使用時必須有返回值,沒有返回值的表示式是不可以使用的。

  使用時一點要注意,考慮好實際情況在進行使用!

二、自動裝箱與自動拆箱

  基本資料型別的自動裝箱(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的原因應該是三目運算子和自動拆箱導致了空指標異常。那麼,這段程式碼為什麼會自動拆箱呢?這其實是三目運算子的語法規範。參見 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。

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

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

  就是保證了三目運算子的第二、第三位運算元都為物件型別。這和三目運算子有關。

五、擴充套件例子

public class Demo2 {
    public static void main(String[] args) {
      // 初始化三個變數
      Integer number1 = 20;
      Integer number2 = 30;
      Integer number3 = null;
      // 表示式
      Integer result = number1 > number2 ? number1 + number2 : number3;
      System.out.printf("三元表示式的結果為:%d", result);
    }
}

  大家能看出來錯誤嗎?可能不仔細看發現不了問題,下面我把控制檯執行結果展示出來。

  看到這個錯誤後我一愣,明明是拿包裝類接收的返回值?不應該報空指標錯誤的啊?然後我將class檔案反編譯了一下,此時錯誤一目瞭然。

  真相大白!當表示式1和表示式2進行型別對齊時丟擲了空指標異常。

  那麼什麼時候會出現型別對齊的情況那?我總結了以下兩種會發出型別對齊的拆箱操作。

1、表示式一和表示式二有一個是基本資料型別。

2、表示式一和表示式二的值型別不一致,會強制拆箱升級成範圍更大的那個表示式的型別。

六、版本差異

  JDK7,報出了空指標異常。

  JDK8,沒有報異常。

  所以還在使用JDK7的小夥伴注意了,為了避免空指標異常,三目運算子中要把基礎型別進行裝箱