最近看到《阿里巴巴Java開發手冊》(公衆號回覆[ 開發手冊 ]免費獲取)第11條規範寫到:

防止 NPE ,是程序員的基本修養

NPE(Null Pointer Exception) 一直是開發中最頭疼的問題,也是最容易忽視的地方。記得剛開始工作的時候所在的項目組線上出現最多的 bug 不是邏輯業務 bug 而是 NPE ,所以後面項目組出了一個奇葩的規矩,線上如果誰出現一個NPE的問題就罰款 100元 ,用作團建費用。如果項目組每個人一個月都出現個兩三個 NPE 的話。那麼項目組是不是每個月都可以去團建下(自己掏錢海喫海喝,心不心疼)。不過自從這個規矩實施以來,線上的 NPE 就漸漸的少了,從最初的一個月團建一次到最後的半年團建一次。大家寫代碼都比較謹慎了,只要用到對象或者集合的時候二話不說上來先判空,所以產生的 NPE 就少了。

業務中返回結果的空值

在我們常見的業務開發中是不是經常會有這樣的接口:

package com.workit.demo.nullexcption;

import com.workit.demo.proxy.User;

import java.util.List;

public interface IUserSearchService {
/**
* 查詢用戶列表
* @return
*/

List<User> listUser();

}

這個接口是不是存在兩個潛在的問題?

  • listUser 這個方法 如果沒有數據,那它是返回空集合還是null呢?
  • getUserById
    ID
    listUser
    
 public List<User> listUser() {
List<User> userList = userRepository.listUser();
if (null == userList || userList.size() == 0) {
return null;
}
return userList;
}

這種實現如果調用者是一個嚴謹的人或者像我這樣被 NPE 罰款買過單的人,是會對返回結果進行 null 的判斷。如果調用者並非謹慎的人或者剛剛入門的人,他就會按照自己的理解去調用接口,拿到結果就不管三七二十一上來對結果就是一頓循環操作,而不進行是否爲 null 的條件判斷,如果這樣的話,是非常危險的,它很有可能出現空指針異常!這就是在代碼中埋了一個定時炸彈,不知道什麼時候就會爆炸。 由於存在這種不安全的隱患我們可以看下第二種實現:

  public List<User> listUser() {
List<User> userList = userRepository.listUser();
if (null == userList || userList.size() == 0) {
return new ArrayList<>();
}
return userList;
}

對於這種實現它一定會返回 List ,即使沒有數據,也會返回一個空集合。通過以上的修改,我們成功的避免了有可能發生的空指針異常,這樣的寫法更安全!那針對於上面的兩種實現,一個是需要調用者進行判空,一個是提供接口的人返回默認值。那我們到底應該用哪種方式呢?這種情況《阿里巴巴開發手冊》也有明確規定: 所以還是那句話使用 任何對象或者集合之前記得先判空

業務中請求參數空值

 /**
* 根據用戶ID查詢當前用戶
* @param id
* @return
*/

User getUserById(Integer id);

這個接口的描述,你能確定入參 id 一定是必傳的嗎?我覺得答案應該是:不能確定。除非接口的文檔註釋上加以說明。那麼我們應該怎樣來約束入參呢?

  • 強制約束

  @Override
public User getUserById(Integer id) {
if (Objects.isNull(id)){
throw new IllegalArgumentException("id不能爲空");
}
return null;
}

通過 jsr 303 進行嚴格的約束聲明配合AOP的操作進行驗證。

User getUserById(@NotNull  Integer id);

其他需要注意的NPE

switch中的空指針異常

看下面的列子妥妥的 NPE

 public static void main(String[] args) {
eat(null);
}
enum EatType{
Breakfast,Lunch,Dinner;
}
public static void eat(EatType eatType){
switch(eatType){
case Breakfast:
System.out.println("喫早飯");
break;
case Lunch:
System.out.println("喫中飯");
break;
case Dinner:
System.out.println("喫晚飯");
break;
default:
System.out.println("輸入錯誤");
break;
}
}

數據庫的sum函數

如果 price 對應的所有的值爲 null ,那麼算出來的和爲 null 如果採用 ifnull 函數就可以求和就是0這樣就可以避免空指針。

使用Map類集合時需要注意存儲值爲 null 的時候

筆者就是由於存儲了 null 值造成生產事故,差點被開除了!詳細介紹可以閱讀以前文章《Java採坑記》

使用 java.util.stream.Collectors 類的 toMap()方法注意value爲空時

如果項目裏面就是有null值怎麼辦呢?可以用下面幾種方法來解決:

  • 過濾值爲null

  • 換一種寫法

  • 據說這個問題 java9 就修復了,所以也可以嘗試升級jdk
  List<Pair<String, Double>> pairArrayList = new ArrayList<>(2);
pairArrayList.add(new Pair<>("version1", 4.22));
pairArrayList.add(new Pair<>("version2", null));
// 第一種過濾值爲null的
Map<String, Double> map = pairArrayList.stream().filter(p-> Objects.nonNull(p.getValue())).collect(
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));
System.out.println(map.toString());
// 換一種實現方式
LinkedHashMap<Object, Object> collect = pairArrayList.stream().collect(LinkedHashMap::new, (m, v) -> m.put(v.getKey(), v.getValue()), LinkedHashMap::putAll);
System.out.println(collect.toString());

輸出結果

{version1=4.22}
{version1=4.22, version2=null}

這個方法還有一個坑 如果key相同也會拋異常 ,感興趣的同學可以動手試試。

使用 Collection 接口任何實現類的 addAll()方法時,都要對輸入的集合參數進行NPE 判斷。

在這裏插入圖片描述

三目運算符可能產生NPE

在這裏插入圖片描述

那麼如何有效的避免NPE呢

  • 使用對象或者集合之前記得先判空。

  • 使用JDK一些API的方法記得要點進源碼去大概看看,不要隨便拿來就用。

  • 單元測試要對空值進行測試,保證程序的健壯性。

  • JDK1.8
    Optional
    NPE
    
  • 提供接口時候需要對非空參數進行說明,並且對非空參數進行校驗,不要太相信調用者。

  • 調用接口的時候一定要對接口返回值進行判空,不要太相信接口提供者。(這個肯定會有值的)。

  • 小心使得萬年船

結束

  • 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。

  • 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。

  • 感謝您的閱讀,十分歡迎並感謝您的關注。

歷史推薦

終於有人把 java代理 講清楚了,萬字詳解!

java採坑之路

超長JVM總結,面試必備

參考

《阿里巴巴泰山版Java開發手冊》(公衆號回覆[ 開發手冊 ]免費獲取)

相關文章