自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

重大線上事故!三元表達(dá)式引發(fā)的空指針問題

開發(fā) 前端
日常開發(fā)中就有可能出現(xiàn)以上 6 種情況。在以上 6 種情況中,如果是涉及到自動拆箱的,一旦包裝類的值為 null,即 null.booleanValue()?,就必然會發(fā)生 NPE(裝箱不會,因為裝箱是 Boolean.valueOf(null),這并不會拋 NPE)。

屬實刺激,剛?cè)肼毑痪镁陀龅竭@種史詩級的線上 Bug,首頁直接崩潰,陳年老代碼爆雷,不管落到最后的底層原因是什么,我感覺主要還是上下游的鏈路太過復(fù)雜,治理難度比較大,牽一發(fā)而動全身。

知識回顧

三目運算符大家都很熟悉了:

<表達(dá)式1> ? <表達(dá)式2> : <表達(dá)式3>

我習(xí)慣稱為三元表達(dá)式,需要注意的就是:**一個三元表達(dá)式從不會既計算 <表達(dá)式 2>,又計算 <表達(dá)式 3>**。條件運算符是右結(jié)合的,也就是說,從右向左分組計算。例如,a ? b : c ? d : e 將按 a ? b : (c ? d : e) 執(zhí)行。

再來回顧下自動拆箱和裝箱機(jī)制,Java 通過這種機(jī)制使得包裝類和基本數(shù)據(jù)類型之間的轉(zhuǎn)換更加方便:

  • 裝箱:將基本數(shù)據(jù)類型轉(zhuǎn)換成包裝類(每個包裝類的構(gòu)造方法都可以接收各自數(shù)據(jù)類型的變量)
  • 拆箱:從包裝類之中取出被包裝的基本類型數(shù)據(jù)(使用包裝類的 xxxValue 方法)

下面以 Integer 為例,我們來看看 Java 內(nèi)置的包裝類是如何進(jìn)行拆裝箱的:

Integer obj = new Integer(10);  // 裝箱
int temp = obj.intValue();   // 拆箱

這種形式的代碼是 JDK 1.5 以前的,JDK 1.5 之后,Java 設(shè)計者為了方便開發(fā)提供了自動裝箱(Autoboxing)與自動拆箱的機(jī)制,并且可以直接利用包裝類的對象進(jìn)行數(shù)學(xué)計算。

還是以 Integer 為例我們來看看自動拆裝箱的過程:

Integer obj = 10;   // 自動裝箱. 基本數(shù)據(jù)類型 int -> 包裝類 Integer
int temp = obj;   // 自動拆箱. Integer -> int
obj ++; // 直接利用包裝類的對象進(jìn)行數(shù)學(xué)計算
System.out.println(temp * obj);

基本數(shù)據(jù)類型到包裝類的轉(zhuǎn)換,不需要像上面一樣使用構(gòu)造函數(shù),直接 = 就完事兒;同樣的,包裝類到基本數(shù)據(jù)類型的轉(zhuǎn)換,也不需要我們手動調(diào)用包裝類的 xxxValue 方法了,直接 = 就能完成拆箱。這也是將它們稱之為自動的原因。

圖片圖片

我們來看看這段代碼反編譯后的文件,底層到底是什么原理:

Integer obj = Integer.valueOf(10);
int temp = obj.intValue();

可以看見,自動裝箱的底層原理其實就是調(diào)用了包裝類的 valueOf 方法,而自動拆箱的底層同樣還是調(diào)用了包裝類的 intValue() 方法。

圖片圖片

問題重現(xiàn)

實際的代碼業(yè)務(wù)邏輯比較復(fù)雜,這里我們舉一個相對簡單的一點的例子先來重現(xiàn)下這個問題:

// 設(shè)置成true,保證條件表達(dá)式的表達(dá)式二一定可以執(zhí)行
boolean flag = true;
//定義一個包裝類對象類型的Boolean變量,值為null 
Boolean nullBoolean = null;
// 定義一個基本數(shù)據(jù)類型的boolean變量
boolean simpleBoolean = false; 

//使用三目運算符并給 x 變量賦值
boolean x = flag ? nullBoolean : simpleBoolean;

以上代碼,在運行過程中,會拋出 NPE:

Exception in thread "main" java.lang.NullPointerException

而且,這個和你使用的 JDK 版本是無關(guān)的,我在 JDK 6、JDK 8 和 JDK 14 上做了測試,均會拋出 NPE。

嘗試對以上代碼進(jìn)行反編譯,使用 jad 工具進(jìn)行反編譯后,得到以下代碼:

boolean flag = true;
boolean simpleBoolean = false;
Boolean nullBoolean = null;

boolean x = flag ? nullBoolean.booleanValue() : simpleBoolean;

可以看到,反編譯后的代碼的最后一行,編譯器幫我們做了一次自動拆箱(nullBoolean 是包裝類,而 x 是基本類型),而 nullBoolean 是 null,這就出現(xiàn)了 null.booleanValue,從而拋出 NPE。

那么,為什么編譯器會進(jìn)行自動拆箱呢?什么情況下需要進(jìn)行自動拆箱呢?

原理分析

關(guān)于為什么編輯器會在代碼編譯階段對于三目運算符中的表達(dá)式進(jìn)行自動拆箱,其實在《The Java Language Specification》(后文簡稱 JLS,是Java 語言規(guī)范,是一切 Java 編程的基礎(chǔ)參照文檔)的第 15.25 章節(jié)中是有相關(guān)介紹的。我們直接看 Java SE 1.7 JLS 中關(guān)于這部分的描述(因為 1.7 的表述更加簡潔一些),原文地址 -> https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.25:

圖片圖片

看我框出來的兩句話:

  1. 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. 當(dāng)?shù)诙缓偷谌徊僮鲾?shù)的類型相同時,則三目運算符表達(dá)式的結(jié)果和這兩位操作數(shù)的類型相同
  2. 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. 當(dāng)?shù)诙?,第三位操作?shù)分別為基本類型和該基本類型對應(yīng)的包裝類型時,那么該表達(dá)式的結(jié)果的類型要求是基本類型

為了滿足以上規(guī)定,又避免程序員過度感知這個規(guī)則,所以在編譯過程中編譯器如果發(fā)現(xiàn)三目操作符的第二位和第三位操作數(shù)的類型分別是基本數(shù)據(jù)類型(如 boolean)以及該基本類型對應(yīng)的包裝類型(如 Boolean)時,并且需要返回表達(dá)式為包裝類型,那么就需要對該包裝類進(jìn)行自動拆箱。

理解下這句話,JLS 的規(guī)范是如果第二和第三位操作數(shù)分別是基本類型和包裝類型,那么要求返回值是基本類型。那如果你自己寫的代碼返回值是包裝類型,那么編譯器為了滿足 JLS 規(guī)范,其實是會自動做一個拆箱的

簡單總結(jié):只要表達(dá)式 1 和表達(dá)式 2 的類型有一個是基本類型一個是包裝類型,就會做觸發(fā)類型對齊的拆箱操作。

下面再列舉幾個例子加深下理解:

boolean flag = true;
boolean simpleBoolean = false;
Boolean objectBoolean = Boolean.FALSE;

當(dāng)?shù)诙缓偷谌槐磉_(dá)式都是包裝類,表達(dá)式返回值也為包裝類,編譯器不需要做拆箱操作

Boolean x1 = flag ? objectBoolean : objectBoolean;

//反編譯后代碼(不需要做任何特殊操作)
Boolean x1 = flag ? objectBoolean : objectBoolean;

當(dāng)?shù)诙缓偷谌槐磉_(dá)式都為基本類型時,表達(dá)式返回值也為基本類型,編譯器不需要做拆箱操作

boolean x2 = flag ? simpleBoolean : simpleBoolean;

//反編譯后代碼(不需要做任何特殊操作)
boolean x2 = flag ? simpleBoolean : simpleBoolean;

當(dāng)?shù)诙缓偷谌槐磉_(dá)式中一個為基本類型另一個為包裝類型時,表達(dá)式返回值為基本類型,編譯器需要做拆箱操作:

boolean x3 = flag ? objectBoolean : simpleBoolean;

//反編譯后代碼(需要對其中的包裝類進(jìn)行拆箱)
boolean x3 = flag ? objectBoolean.booleanValue() : simpleBoolean;

如果你清楚三目運算符的規(guī)則,那你就會正確地按照以上方式去定義 x1、x2 和 x3 的類型。

但是,并不是所有人都熟知這個規(guī)則,所以在實際應(yīng)用中,還會出現(xiàn)以下幾種定義方式:

boolean x4 = flag ? objectBoolean : objectBoolean;

// 反編譯后代碼(三元表達(dá)式的結(jié)果要求是包裝類,而 x4 是基本類型,所以編譯器需要做拆箱)
boolean x4 = (flag ? objectBoolean : objectBoolean).booleanValue();
Boolean x5 = flag ? simpleBoolean : simpleBoolean;

// 反編譯后代碼(三元表達(dá)式的結(jié)果要求是基本類型,而 x5 是包裝類型,所以編譯器需要做裝箱)
Boolean x5 = Boolean.valueOf(flag ? simpleBoolean : simpleBoolean);
Boolean x6 = flag ? objectBoolean : simpleBoolean;

// 反編譯后代碼(三元表達(dá)式的結(jié)果要求是基本類型,而 x5 是包裝類型,所以編譯器需要做裝箱)
Boolean x6 = Boolean.valueOf(flag ? objectBoolean.booleanValue() : simpleBoolean);

所以,日常開發(fā)中就有可能出現(xiàn)以上 6 種情況。在以上 6 種情況中,如果是涉及到自動拆箱的,一旦包裝類的值為 null,即 null.booleanValue(),就必然會發(fā)生 NPE(裝箱不會,因為裝箱是 Boolean.valueOf(null),這并不會拋 NPE)。

小伙伴們可以把以上的 x3、x4 以及 x6 中的的包裝類設(shè)置成 null,看看是不是會拋 NPE:

boolean flag = true;
boolean simpleBoolean = false;
Boolean objectBoolean = Boolean.FALSE;
// 將包裝類設(shè)置為 null
Boolean nullBoolean = null;

boolean x3 = flag ? nullBoolean : simpleBoolean;
boolean x4 = flag ? nullBoolean : objectBoolean;
Boolean x6 = flag ? nullBoolean : simpleBoolean;

以上三種情況,都會在執(zhí)行時發(fā)生 NPE:

  • 其中 x3 和 x6 是三目運算符運算過程中,根據(jù) JLS 的規(guī)則確定類型的過程中要做自動拆箱而導(dǎo)致的 NPE。由于使用了三目運算符,并且第二、第三位操作數(shù)分別是基本類型和對象。就需要對對象進(jìn)行拆箱操作,由于該對象為 null,所以在拆箱過程中調(diào)用 null.booleanValue() 的時候就報了 NPE。
  • 而 x4 是因為三目運算符運算結(jié)束后根據(jù)規(guī)則他得到的是一個對象類型,但是在給變量賦值過程中進(jìn)行自動拆箱所導(dǎo)致的 NPE。
責(zé)任編輯:武曉燕 來源: 飛天小牛肉
相關(guān)推薦

2020-10-14 10:18:05

Python三元表達(dá)式代碼

2023-11-30 08:30:12

Python三元表達(dá)

2023-09-06 09:40:29

2018-10-08 08:00:00

前端ReactJavaScript

2020-04-21 08:24:09

IO機(jī)器代碼

2020-05-07 11:00:24

Go亂碼框架

2023-02-16 08:55:13

2017-08-25 16:38:05

表達(dá)式正則血案

2023-12-31 12:06:51

2014-01-05 17:41:09

PostgreSQL表達(dá)式

2020-04-02 07:31:53

RPC超時服務(wù)端

2024-03-25 13:46:12

C#Lambda編程

2022-01-04 23:13:57

語言PanicGolang

2010-03-03 12:53:50

Linux正則表達(dá)式

2018-09-27 15:25:08

正則表達(dá)式前端

2010-07-19 16:11:20

Perl正則表達(dá)式

2012-12-18 16:38:26

2010-07-14 09:24:22

Perl正則表達(dá)式

2009-08-19 14:48:57

正則表達(dá)式性能

2012-06-26 10:03:58

JavaJava 8lambda
點贊
收藏

51CTO技術(shù)棧公眾號