《新版阿里巴巴Java开发手册》提到的三目运算符的空指针问题到底是个怎么回事?
最近,阿里巴巴Java開發(fā)手冊發(fā)布了最新版——泰山版,這個名字起的不錯,一覽眾山小。
新版新增了30+規(guī)約,其中有一條規(guī)約引起了作者的關(guān)注,那就是手冊中提到在三目運算符使用過程中,需要注意自動拆箱導(dǎo)致的NullPointerException(后文簡稱:NPE)問題:
因為這個問題我很久之前(2015年)遇到過,曾經(jīng)在博客中也記錄過,剛好最新的開發(fā)手冊再次提到了這個知識點,于是把之前的文章內(nèi)容翻出來并重新整理了一下,帶大家一起回顧下這個知識點。
可能有些人看過我之前那篇文章,本文并不是單純的"舊瓶裝新酒",在重新梳理這個知識點的時候,作者重新翻閱了《The Java Language Specification》,并且對比了Java SE 7 和 Java SE 8之后的相關(guān)變化,希望可以幫助大家更加全面的理解這個問題。
基礎(chǔ)回顧
在詳細(xì)展看介紹之前,先簡單介紹下本文要涉及到的幾個重要概念,分別是"三目運算符"、"自動拆裝箱"等,如果大家對于這些歷史知識有所掌握的話,可以先跳過本段內(nèi)容,直接看問題重現(xiàn)部分即可。
三目運算符
在《The Java Language Specification》中,三目運算符的官方名稱是 Conditional Operator ? : ,我一般稱呼他為條件表達(dá)式,詳細(xì)介紹在JLS 15.25中,這里簡單介紹下其基本形式和用法:
三目運算符是Java語言中的重要組成部分,它也是唯一有3個操作數(shù)的運算符。形式為:
<表達(dá)式1> ? <表達(dá)式2> : <表達(dá)式3>以上,通過?、:組合的形式得到一個條件表達(dá)式。其中?運算符的含義是:先求表達(dá)式1的值,如果為真,則執(zhí)行并返回表達(dá)式2的結(jié)果;如果表達(dá)式1的值為假,則執(zhí)行并返回表達(dá)式3的結(jié)果。
值得注意的是,一個條件表達(dá)式從不會既計算<表達(dá)式2>,又計算<表達(dá)式3>。條件運算符是右結(jié)合的,也就是說,從右向左分組計算。例如,a?b:c?d:e將按a?b:(c?d:e)執(zhí)行。
自動裝箱與自動拆箱
介紹過了三目運算符(條件表達(dá)式)之后,我們再來簡單介紹下Java中的自動拆裝箱相關(guān)知識點。
每一個Java開發(fā)者一定都對Java中的基本數(shù)據(jù)類型不陌生,Java中共有8種基本數(shù)據(jù)類型,這些基礎(chǔ)數(shù)據(jù)類型帶來一個好處就是他們直接在棧內(nèi)存中存儲,不會在堆上分配內(nèi)存,使用起來更加高效。
但是,Java語言是一個面向?qū)ο蟮恼Z言,而基本數(shù)據(jù)類型不是對象,導(dǎo)致在實際使用過程中有諸多不便,如集合類要求其內(nèi)部元素必須是Object類型,基本數(shù)據(jù)類型就無法使用。
所以,相對應(yīng)的,Java提供了8種包裝類型,更加方便在需要對象的地方使用。
有了基本數(shù)據(jù)類型和包裝類,帶來了一個麻煩就是需要在他們之間進(jìn)行轉(zhuǎn)換。在Java SE5中,為了減少開發(fā)人員的工作,Java提供了自動拆箱與自動裝箱功能。
自動裝箱: 就是將基本數(shù)據(jù)類型自動轉(zhuǎn)換成對應(yīng)的包裝類。
自動拆箱:就是將包裝類自動轉(zhuǎn)換成對應(yīng)的基本數(shù)據(jù)類型。
Integer i =10; //自動裝箱 int b= i; //自動拆箱我們可以簡單理解為,當(dāng)我們自己寫的代碼符合裝(拆)箱規(guī)范的時候,編譯器就會自動幫我們拆(裝)箱。
自動裝箱都是通過包裝類的valueOf()方法來實現(xiàn)的.自動拆箱都是通過包裝類對象的xxxValue()來實現(xiàn)的(如booleanValue()、longValue()等)。
問題重現(xiàn)
在最新版的開發(fā)手冊中給出了一個例子,提示我們在使用三目運算符的過程中,可能會進(jìn)行自動拆箱而導(dǎo)致NPE問題。
原文中的例子相對復(fù)雜一些,因為他還涉及到多個Integer相乘的結(jié)果是int的問題,我們舉一個相對簡單的一點的例子先來重現(xiàn)下這個問題:
boolean flag = true; //設(shè)置成true,保證條件表達(dá)式的表達(dá)式二一定可以執(zhí)行 boolean simpleBoolean = false; //定義一個基本數(shù)據(jù)類型的boolean變量 Boolean nullBoolean = null;//定義一個包裝類對象類型的Boolean變量,值為nullboolean x = flag ? nullBoolean : simpleBoolean; //使用三目運算符并給x變量賦值以上代碼,在運行過程中,會拋出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;可以看到,反編譯后的代碼的最后一行,編譯器幫我們做了一次自動拆箱,而就是因為這次自動拆箱,導(dǎo)致代碼出現(xiàn)對于一個null對象(nullBoolean.booleanValue())的調(diào)用,導(dǎo)致了NPE。
那么,為什么編譯器會進(jìn)行自動拆箱呢?什么情況下需要進(jìn)行自動拆箱呢?
原理分析
關(guān)于為什么編輯器會在代碼編譯階段對于三目運算符中的表達(dá)式進(jìn)行自動拆箱,其實在《The Java Language Specification》(后文簡稱JLS)的第15.25章節(jié)中是有相關(guān)介紹的。
在不同版本的JLS中,關(guān)于這部分描述雖然不盡相同,尤其在Java 8中有了大幅度的更新,但是其核心內(nèi)容和原理是不變的。我們直接看Java SE 1.7 JLS中關(guān)于這部分的描述(因為1.7的表述更加簡潔一些):
The type of a conditional expression is determined as follows: ? 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.
簡單的來說就是:當(dāng)?shù)诙缓偷谌徊僮鲾?shù)的類型相同時,則三目運算符表達(dá)式的結(jié)果和這兩位操作數(shù)的類型相同。當(dāng)?shù)诙?#xff0c;第三位操作數(shù)分別為基本類型和該基本類型對應(yīng)的包裝類型時,那么該表達(dá)式的結(jié)果的類型要求是基本類型。
為了滿足以上規(guī)定,又避免程序員過度感知這個規(guī)則,所以在編譯過程中編譯器如果發(fā)現(xiàn)三目操作符的第二位和第三位操作數(shù)的類型分別是基本數(shù)據(jù)類型(如boolean)以及該基本類型對應(yīng)的包裝類型(如Boolean)時,并且需要返回表達(dá)式為包裝類型,那么就需要對該包裝類進(jìn)行自動拆箱。
在Java SE 1.8 JLS中,關(guān)于這部分描述又做了一些細(xì)分,再次把表達(dá)式區(qū)分成布爾型條件表達(dá)式(Boolean Conditional Expressions)、數(shù)值型條件表達(dá)式(Numeric Conditional Expressions)和引用類型條件表達(dá)式(Reference Conditional Expressions)。
并且通過表格的形式明確的列舉了第二位和第三位分別是不同類型時得到的表達(dá)式結(jié)果值應(yīng)該是什么,感興趣的大家可以去翻閱一下。
其實簡單總結(jié)下,就是:當(dāng)?shù)诙缓偷谌槐磉_(dá)式都是包裝類型的時候,該表達(dá)式的結(jié)果才是該包裝類型,否則,只要有一個表達(dá)式的類型是基本數(shù)據(jù)類型,則表達(dá)式得到的結(jié)果都是基本數(shù)據(jù)類型。如果結(jié)果不符合預(yù)期,那么編譯器就會進(jìn)行自動拆箱。(即Java開發(fā)手冊中總結(jié)的:只要表達(dá)式1和表達(dá)式2的類型有一個是基本類型,就會做觸發(fā)類型對齊的拆箱操作,只不過如果都是基本類型也就不需要拆箱了。)
如下3種情況是我們熟知該規(guī)則,在聲明表達(dá)式的結(jié)果的類型時刻意和規(guī)則保持一致的情況(為了幫助大家理解,我備注了注釋和反編譯后的代碼):
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; //因為x1的類型是對象,所以不需要做任何特殊操作。//當(dāng)?shù)诙缓偷谌槐磉_(dá)式都為基本類型時,表達(dá)式返回值也為基本類型; boolean x2 = flag ? simpleBoolean : simpleBoolean; //反編譯后代碼為:boolean x2 = flag ? simpleBoolean : simpleBoolean; //因為x2的類型也是基本類型,所以不需要做任何特殊操作。//當(dāng)?shù)诙缓偷谌槐磉_(dá)式中有一個為基本類型時,表達(dá)式返回值也為基本類型; boolean x3 = flag ? objectBoolean : simpleBoolean; //反編譯后代碼為:boolean x3 = flag ? objectBoolean.booleanValue() : simpleBoolean; //因為x3的類型是基本類型,所以需要對其中的包裝類進(jìn)行拆箱。因為我們熟知三目運算符的規(guī)則,所以我們就會按照以上方式去定義x1、x2和x3的類型。
但是,并不是所有人都熟知這個規(guī)則,所以在實際應(yīng)用中,還會出現(xiàn)以下三種定義方式:
//當(dāng)?shù)诙缓偷谌槐磉_(dá)式都是對象時,表達(dá)式返回值也為對象; boolean x4 = flag ? objectBoolean : objectBoolean; //反編譯后代碼為:boolean x4 = (flag ? objectBoolean : objectBoolean).booleanValue(); //因為x4的類型是基本類型,所以需要對表達(dá)式結(jié)果進(jìn)行自動拆箱。//當(dāng)?shù)诙缓偷谌槐磉_(dá)式都為基本類型時,表達(dá)式返回值也為基本類型; Boolean x5 = flag ? simpleBoolean : simpleBoolean; //反編譯后代碼為:Boolean x5 = Boolean.valueOf(flag ? simpleBoolean : simpleBoolean); //因為x5的類型是對象類型,所以需要對表達(dá)式結(jié)果進(jìn)行自動裝箱。//當(dāng)?shù)诙缓偷谌槐磉_(dá)式中有一個為基本類型時,表達(dá)式返回值也為基本類型; Boolean x6 = flag ? objectBoolean : simpleBoolean; //反編譯后代碼為:Boolean x6 = Boolean.valueOf(flag ? objectBoolean.booleanValue() : simpleBoolean); //因為x6的類型是對象類型,所以需要對表達(dá)式結(jié)果進(jìn)行自動裝箱。所以,日常開發(fā)中就有可能出現(xiàn)以上6種情況。聰明的讀者們讀到這里也一定想到了,在以上6種情況中,如果是涉及到自動拆箱的,一旦對象的值為null,就必然會發(fā)生NPE。
舉例驗證,我們把以上的x3、x4以及x6中的的對象類型設(shè)置成null,分別執(zhí)行下代碼:
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。
小結(jié)
如前文介紹,在開發(fā)過程中,如果涉及到三目運算符,那么就要高度注意其中的自動拆裝箱問題。
最好的做法就是保持三目運算符的第二位和第三位表達(dá)式的類型一致,并且如果要把三目運算符表達(dá)式給變量賦值的時候,也盡量保持變量的類型和他們保持一致。并且,做好單元測試!!!
所以,Java開發(fā)手冊中提到要高度注意第二位和第三位表達(dá)式的類型對齊過程中由于自動拆箱發(fā)生的NPE問題,其實還需要注意使用三目運算符表達(dá)式給變量賦值的時候由于自動拆箱導(dǎo)致的NPE問題。
至此,我們已經(jīng)介紹完了Java開發(fā)手冊中關(guān)于三目運算符使用過程中可能會導(dǎo)致NPE的問題。
如果一定要給出一個方法論去避免這個問題的話,那么在使用的過程中,無論是三目運算符中的三個表達(dá)式,還是三目運算符表達(dá)式要賦值的變量,最好都使用包裝類型,可以減少發(fā)生錯誤的概率。
正文內(nèi)容已完,如果大家對這個問題還有更深的興趣的話,接下來部分內(nèi)容是擴(kuò)展內(nèi)容,也歡迎學(xué)習(xí),不過這部分涉及到很多JLS的規(guī)范,如果實在看不懂也沒關(guān)系~
擴(kuò)展思考
為了方便大家理解,我使用了簡單的布爾類型的例子說明了NPE的問題。但是實際在代碼開發(fā)中,遇到的場景可能并沒有那么簡單,比如說以下代碼,大家猜一下能否正常執(zhí)行:
Map<String,Boolean> map = new HashMap<String, Boolean>(); Boolean b = (map!=null ? map.get("Hollis") : false);如果你的答案是"不能,這里會拋NPE"那么說明你看懂了本文的內(nèi)容,但是,我只能說你只是答對了一半。
因為以上代碼,在小于JDK 1.8的版本中執(zhí)行的結(jié)果是NPE,在JDK 1.8 及以后的版本中執(zhí)行結(jié)果是null。
之所以會出現(xiàn)這樣的不同,這個就說來話長了,我挑其中的重點內(nèi)容簡單介紹下吧,以下內(nèi)容主要內(nèi)容還是圍繞Java 8 的JLS 。
JLS 15中對條件表達(dá)式(三目運算符)做了細(xì)分之后分為三種,區(qū)分方式:
如果表達(dá)式的第二個和第三個操作數(shù)都是布爾表達(dá)式,那么該條件表達(dá)式就是布爾表達(dá)式
如果表達(dá)式的第二個和第三個操作數(shù)都是數(shù)字型表達(dá)式,那么該條件表達(dá)式就是數(shù)字型表達(dá)式
除了以上兩種以外的表達(dá)式就是引用表達(dá)式
因為Boolean b = (map!=null ? map.get("Hollis") : false);表達(dá)式中,第二位操作數(shù)為map.get("test"),雖然Map在定義的時候規(guī)定了其值類型為Boolean,但是在編譯過程中泛型是會被擦除的(泛型的類型擦除),所以,其結(jié)果就是Object。那么根據(jù)以上規(guī)則判斷,這個表達(dá)式就是引用表達(dá)式。
又跟據(jù)JLS15.25.3中規(guī)定:
如果引用條件表達(dá)式出現(xiàn)在賦值上下文或調(diào)用上下文中,那么條件表達(dá)式就是合成表達(dá)式
因為,Boolean b = (map!=null ? map.get("Hollis") : false);其實就是一個賦值上下文(關(guān)于賦值上下文相見JLS 5.2),所以map!=null ? map.get("Hollis") : false;就是合成表達(dá)式。
那么JLS15.25.3中對合成表達(dá)式的操作數(shù)類型做了約束:
合成的引用條件表達(dá)式的類型與其目標(biāo)類型相同
所以,因為有了這個約束,編譯器就可以推斷(Java 8 中類型推斷,詳見JLS 18)出該表達(dá)式的第二個操作數(shù)和第三個操作數(shù)的結(jié)果應(yīng)該都是Boolean類型。
所以,在編譯過程中,就可以分別把他們都轉(zhuǎn)成Boolean即可,那么以上代碼在Java 8中反編譯后內(nèi)容如下:
Boolean b = maps == null ? Boolean.valueOf(false) : (Boolean)maps.get("Hollis");但是在Java 7中可沒有這些規(guī)定(Java 8之前的類型推斷功能還很弱),編譯器只知道表達(dá)式的第二位和第三位分別是基本類型和包裝類型,而無法推斷最終表達(dá)式類型。
那么他就會先根據(jù)JLS 15.25的規(guī)定,把返回值結(jié)果轉(zhuǎn)換成基本類型。然后在進(jìn)行變量賦值的時候,再轉(zhuǎn)換成包裝類型:
Boolean b = Boolean.valueOf(maps == null ? false : ((Boolean)maps.get("Hollis")).booleanValue());所以,相比Java 8中多了一步自動拆箱,所以會導(dǎo)致NPE。
《解讀Java開發(fā)手冊》電子書來了,靈魂13問,深入剖析Java規(guī)約背后的原理,從"問題重現(xiàn)"到"原理分析"再到"問題解決",深入挖掘阿里巴巴開發(fā)思維!《Java開發(fā)手冊》必備伴讀書目。
關(guān)注公眾號,后臺回復(fù)『Java手冊』即可下載。
參考資料:
《Java開發(fā)手冊——泰山版》
http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.25
http://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.25
https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.2
https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.2.7
https://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html
總結(jié)
以上是生活随笔為你收集整理的《新版阿里巴巴Java开发手册》提到的三目运算符的空指针问题到底是个怎么回事?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 简单的负载均衡器
- 下一篇: 2020年Java程序员应该学习的10大