萬惡的NPE差點讓我半個月工資沒了
引言
最近看到《阿里巴巴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
沒有找到用戶,是拋異常還是返回null呢?
首先我們先看下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開發手冊》