代碼炸了
前一段時間,項目緊急疊代,臨時加入了一個新功能:使用者通過浏覽器在系統界面上操作,然後Java背景代碼做一些資料的查詢、計算和整合的工作,并對第三方提供了操作接口。
當晚淩晨上線,本系統内測試,完美通過!
第二天将接口對外提供,供第三方系統調用,duang!工單立馬來了。

很明顯,背景代碼炸了!拉了一下背景日志,原來又是煩人的空指針異常
NullPointerException
!
為此,本文痛定思痛,關于
null
空指針異常問題的預防和解決,詳細整理成文,并嚴格反思:我們到底在代碼中應該如何防止空指針異常所導緻的Bug?
最常見的輸入判空
對輸入判空非常有必要,并且常見,舉個栗子:
public String addStudent( Student student ) {
// ...
}
複制
無論如何,你在進行函數内部業務代碼編寫之前一定會對傳入的
student
對象本身以及每個字段進行判空或校驗:
public String addStudent( Student student ) {
if( student == null )
return "傳入的Student對象為null,請傳值";
if( student.getName()==null || "".equals(student.getName()) )
return "傳入的學生姓名為空,請傳值";
if( student.getScore()==null )
return "傳入的學生成績為null,請傳值";
if( (student.getScore()<0) || (student.getScore()>100) )
return "傳入的學生成績有誤,分數應該在0~100之間";
if( student.getMobile()==null || "".equals(student.getMobile()) )
return "傳入的學生電話号碼為空,請傳值";
if( student.getMobile().length()!=11 )
return "傳入的學生電話号碼長度有誤,應為11位";
studentService.addStudent( student ); // 将student對象存入MySQL資料庫
return "SUCCESS";
}
複制
手動空指針保護
手動進行
if(obj !=null)
的判空自然是最全能的,也是最可靠的,但是怕就怕俄羅斯套娃式的
if
判空。
舉例一種情況:
為了擷取:
省(Province)→市(Ctiy)→區(District)→街道(Street)→道路名(Name)
作為一個“嚴謹且良心”的後端開發工程師,如果手動地進行空指針保護,我們難免會這樣寫:
public String getStreetName( Province province ) {
if( province != null ) {
City city = province.getCity();
if( city != null ) {
District district = city.getDistrict();
if( district != null ) {
Street street = district.getStreet();
if( street != null ) {
return street.getName();
}
}
}
}
return "未找到該道路名";
}
複制
為了擷取到鍊條最終端的目的值,直接鍊式取值必定有問題,因為中間隻要某一個環節的對象為
null
,則代碼一定會炸,并且抛出
NullPointerException
異常,然而俄羅斯套娃式的
if
判空實在有點心累。
消除俄羅斯套娃式判空
Optional
接口本質是個容器,你可以将你可能為
null
的變量交由它進行托管,這樣我們就不用顯式對原變量進行
null
值檢測,防止出現各種空指針異常。
Optional文法專治上面的俄羅斯套娃式
if
判空,是以上面的代碼可以重構如下:
public String getStreetName( Province province ) {
return Optional.ofNullable( province )
.map( i -> i.getCity() )
.map( i -> i.getDistrict() )
.map( i -> i.getStreet() )
.map( i -> i.getName() )
.orElse( "未找到該道路名" );
}
複制
漂亮!嵌套的
if/else
判空灰飛煙滅!
解釋一下執行過程:
-
:它以一種智能包裝的方式來構造一個ofNullable(province )
執行個體,Optional
是否為province
均可以。如果為null
,傳回一個單例空null
對象;如果非Optional
,則傳回一個null
包裝對象Optional
-
:該函數主要做值的轉換,如果上一步的值非map(xxx )
,則調用括号裡的具體方法進行值的轉化;反之則直接傳回上一步中的單例null
包裝對象Optional
-
:很好了解,在上面某一個步驟的值轉換終止時進行調用,給出一個最終的預設值orElse(xxx )
當然實際代碼中倒很少有這種極端情況,不過普通的
if(obj !=null)
判空也可以用
Optional
文法進行改寫,比如很常見的一種代碼:
List<User> userList = userMapper.queryUserList( userType );
if( userList != null ) {//此處免不了對userList進行判空
for( User user : userList ) {
// ...
// 對user對象進行操作
// ...
}
}
複制
如果用
Optional
接口進行改造,可以寫為:
List<User> userList = userMapper.queryUserList( userType );
Optional.ofNullable( userList ).ifPresent(
list -> {
for( User user : list ) {
// ...
// 對user對象進行操作
// ...
}
}
)
複制
這裡的
ifPresent()
的含義很明顯:僅在前面的
userList
值不為
null
時,才做下面其餘的操作。
隻是一顆文法糖
沒有用過
Optional
文法的小夥伴們肯定感覺上面的寫法非常甜蜜!然而褪去華麗的外衣,甜蜜的
Optional
文法底層依然是樸素的語言級寫法,比如我們看一下
Optional
的
ifPresent()
函數源碼,就是普通的
if
判斷而已:
那就有人問:我們何必多此一舉,做這樣一件無聊的事情呢?
其實不然!
用
Optional
來包裝一個可能為
null
值的變量,其最大意義其實僅僅在于給了調用者一個明确的警示!
怎麼了解呢?
比如你寫了一個函數,輸入學生學号
studentId
,給出學生的得分 :
Score getScore( Long studentId ) {
// ...
}
複制
調用者在調用你的方法時,一旦忘記
if(score !=null)
判空,那麼他的代碼肯定是有一定
bug
幾率的。
但如果你用
Optional
接口對函數的傳回值進行了包裹:
Optional<Score> getScore( Long studentId ) {
// ...
}
複制
這樣當調用者調用這個函數時,他可以清清楚楚地看到
getScore()
這個函數的傳回值的特殊性(有可能為
null
),這樣一個警示一定會很大幾率上幫助調用者規避
null
指針異常。
老項目該怎麼辦?
上面所述的
Optional
文法隻是在
JDK 1.8
版本後才開始引入,那還在用
JDK 1.8
版本之前的老項目怎麼辦呢?
沒關系!
Google
大名鼎鼎的
Guava
庫中早就提供了
Optional
接口來幫助優雅地處理
null
對象問題,其本質也是在可能為
null
的對象上做了一層封裝,使用起來和JDK本身提供的
Optional
接口沒有太大差別。
你隻需要在你的項目裡引入
Google
的
Guava
庫:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
複制
即可享受到和
Java8
版本開始提供的
Optional
一樣的待遇!