聊一聊日常開發中如何優雅的避免那無處不在的空指標異常

码农Academy發表於2024-03-01

在Java程式語言中,NullPointerException(簡稱NPE)是一種常見的執行時異常,當程式試圖訪問或操作一個還未初始化(即值為null)的物件引用時,Java虛擬機器就會丟擲NullPointerException。如果我們在日常開發中,不能很好的去規避NPE,那麼可能因為資料或者其他問題就會導致線上問題。。。很煩。。。。

阿里巴巴開發手冊規約中也說明防止NPE,是程式設計師的基本素養。。。
image.png

接下來我們先談談幾種可能會出現空指標異常的方式。

出現空指標異常的情況

  • 訪問空物件的屬性或呼叫空物件的方法
    當一個物件是null時,試圖訪問一個物件的屬性或呼叫其方法,就會觸發空指標異常。
String text = null;
int length = text.length();

User user = null;
String userName = user.getUserName();
  • 陣列為null或者陣列元素為null
    當嘗試訪問陣列中的某個索引處的元素,而該元素為null時,同樣會導致空指標異常。
String[] strs = null;
int length = strs.length;

String[] strs = new String[3];  
int length = strs[2].length();
  • 集合中null元素訪問
    當集合中存在null元素,當我們遍歷集合,訪問到這個元素的屬性或者方法時也會丟擲NPE,這種情況也會出現在我們的日常開發中,有時候就會因為資料問題導致這種情況發生,常常也莫名其妙。。。。
List<String> list = Lists.newArrayList();  
list.add(null);  
System.out.println(list.get(0).length());
  • 呼叫的方法返回null
    呼叫某個方法,期望其返回一個非null的物件,但實際返回了null。當然這種情況等同於訪問空物件的屬性或者方法。這在實際開發過程中極易出現的一種情況。比如我們使用Mybatis從資料庫中查詢一條記錄時,資料不存在,就會返回null。這種情況尤為注意。
private User getUserInfo(){  
    return null;  
}

User user = getUserInfo();
String userName = user.getUserName();
  • 使用基本資料型別的包裝類
    在使用基本資料型別的包裝類時,如果未正確初始化,再轉成int時,可能導致空指標異常。
Integer i = null;  
int num = i;

以上大概是我想到或者常遇到的一些可能會發生NPE的情況,如果還有其他情況,可以貼出來討論。

那麼我們該如何避免NPE呢?

避免NPE的幾種方式

  • 訪問物件前要謹慎
    在使用物件之前,始終檢查它是否為null。這包括方法引數、返回值以及物件的屬性。在訪問物件的方法或屬性之前,使用條件語句判斷物件是否為null。比如我們在訪問User物件前,一定要判null
User user = new User();  
if (user != null){  
    String userName = user.getUserName();  
    Address address = user.getAddress();  
    if (address != null){  
        String coutry = address.getCountry();  
    }  
}

或者我們的user是從一個方法中獲取的,例如資料庫中查詢,那麼我們在訪問這個物件前,一定要判null,如果為null要丟擲對應的業務異常,然後我們就可以在介面響應中對應返回錯誤的資訊即可,此時就算是一個正常的流程了。這點尤為重要,一定要注意。

User user = userManager.getUserById(Long userId);
if (user == null){
	throw new ServiceException(""當前查詢的物件不存在);
}

關於SpringBoot專案中捕獲自定義業務異常,統一異常管理,統一結果返回,可以參考這篇文章:SpringBoot統一結果返回,統一異常處理,大牛都這麼玩 | 碼農Academy的部落格

當然如果使我們在寫User getUserById(Long id)返回物件或者List<User> listUserByIds(List<Long> idList)時我們可以不返回null,可以返回一個物件預設資訊或者一個空集合,這樣呼叫方就不會出現NPE風險,當然我們不強制返回一個物件或者空集合,但是必須新增註釋充分 說明什麼情況下會返回null值。這也是阿里巴巴開發手冊規約的建議。
image.png

  • 使用Optional類
    JDK8以上版本提供了Optional類,它是一個容器物件,可用於包裝可能為null的值。我們可以使用它判斷null問題,同時也解決了多層級訪問問題,配合使用orElse時,會先執行orElse方法,然後執行邏輯程式碼,不管是否出現了空指標。
String country = Optional.ofNullable(user)  
        .map(User::getAddress)  
        .map(Address::getCountry)  
        .orElse("");

String country = Optional.ofNullable(user)  
        .map(User::getAddress)  
        .map(Address::getCountry)  
        .orElseGet(() -> defaultContry());

private String defaultContry(){
	return "CN";
}

我們還可以使用orElseThrow()方法,當Optional中的物件是一個null時我們直接丟擲異常:

String userName = Optional.ofNullable(user).map(User::getUserName).orElseThrow(() -> new ServiceException("當前使用者資訊不存在"));
  • 使用斷言避免空指標
    使用Java斷言(assert)來檢查變數是否為null。但要注意,斷言通常在開發和測試階段啟用,而在生產環境中可能被禁用(在生產環境中,通常不會啟用斷言以避免不必要的效能開銷以及防止潛在的錯誤資訊洩漏)。
User user = new User();
assert user != null : "user should not be null";
Address address = user.getAddress();  
assert address != null : "address should not be null";  
String coutry = address.getCountry();  
  • 使用@Nullable註解
    使用javax.annotation.Nullable註解,@Nullable註解通常用於標記一個方法的引數、返回值或者欄位可能為null。這個註解並非Java標準庫的一部分,但在一些第三方庫(如JSR 305庫中的javax.annotation.Nullable,以及Google Guava和JetBrains的Kotlin標準庫等)中廣泛使用,並且被許多IDE和靜態分析工具支援。以便在編譯期或開發工具中提示可能的NPE風險。
@Nullable  
private static User getUserById(Long userId){  
    return null;  
}  
  
private static void handlerUser(@Nullable User user){  
    System.out.println(user.getUserName());  
}

public static void main(String[] args) {  
  Long userId = 0L;  
  User user = getUserById(userId);  
  String userName = user.getUserName();  
  handlerUser(user);  
}

此時IDEA就會警告會出現NPE風險
image.png

  • 藉助工具掃描程式碼
    在Java開發中,我們還可以使用以下工具掃描程式碼以發現潛在的空指標異常風險。
  1. IntelliJ IDEA:內建了強大的靜態程式碼分析器,能夠檢測出可能的NPE和其他程式碼問題。
  2. SonarQube / SonarLint:提供持續整合和本地IDE外掛形式的靜態程式碼分析,能找出潛在的空指標以及其他質量或安全問題。Sonar可以定時掃描倉庫中的程式碼,可以發現程式碼中的一些潛在風險,可以透過一些通知例如郵件等告知程式碼提交者這段程式碼的風險。
  3. FindBugs(現更名為SpotBugs):另一個開源的靜態分析工具,能夠發現潛在的bug,包括可能導致NPE的情況。
  4. 阿里巴巴Java開發規約外掛: 對於Eclipse和IntelliJ IDEA都有相應的外掛版本,基於阿里巴巴內部Java編碼規範,包含了對可能出現NPE情況的檢測。

補充一點

在JDK 17中引入的Helpful NullPointerExceptions特性確實增強了空指標異常資訊的準確性與可用性。當發生NullPointerException時,JVM現在能夠提供更精確的位置資訊,特別是在鏈式呼叫場景下,它會指出導致空指標異常的具體物件引用。這有助於開發者更快地定位到程式碼中的問題所在,無需透過堆疊跟蹤逐層分析來判斷哪個物件引用為null。
假如我們訪問user.getAddress().getCountry().length()時,在JDK17以前,如果發生了空指標異常,他只會列印出來發生了空指標異常,但是並沒有告知到底是user物件還是address物件還是coutnry發生了異常:

Exception in thread "main" java.lang.NullPointerException
	at com.study.base.core.base.NpeTest.main(NpeTest.java:23)

但是在JDK17以後,藉助Helpful NullPointerExceptions特性,異常資訊將更加精確,可能會類似列印這樣的資訊,精確到那個值發生了空指標異常:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Address.getCountry()" because "user.address" is null
	at com.study.base.core.base.NpeTest.main(NpeTest.java:23)

這又多了一個升級JDK到17以上的理由。

結論

NullPointerException(NPE)是Java開發中常見的執行時異常,源於對未初始化或已置為null的物件引用進行操作。在實際開發過程中,進行非空檢查、使用Optional類以及採用Null安全註解以及使用檢查工具等策略可以有效避免此類異常的發生。

本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等

相關文章