新手業務編碼中常見的null值和空指針處理
背景
大家都知道,使用對象的時候,由于對象的默認值為null, 如果沒有及時判空就去調用對象的方法,可能會帶來空指針異常的問題。本篇將會講解空指針異常容易在哪些情況下出現,新手應該如何去避免無處不在的null值問題,又應該如何修復。主要舉一些常見的例子來配合說明。
1、自動拆箱導致的空指針異常
首先,自動拆箱有兩種場景,第一種是包裝類型的變量賦值給基本類型的變量時,會出現自動拆箱;
Integer num = 10; // 自動裝箱
int value = num; // 自動拆箱
System.out.println(value); // 輸出:10
第二種是基本類型作為方法的形參,而這個方法被調用時,傳入的參數是包裝類的變量,就會發生自動拆箱,如下所示:
public class AutoUnboxExample {
public static void printInt(int num) {
System.out.println(num);
}
public static void main(String[] args) {
Integer number = new Integer(20); // 創建一個Integer對象
printInt(number); // 自動拆箱:將Integer對象轉換為int類型
}
}
那自動拆箱什么時候會發生空指針異常呢?異常的原因是什么?
首先,賦值的時候,
Integer num = null; // 自動裝箱
int value = num; // 自動拆箱, 將null 值賦給了value。此時必定會報空指針異常。
javap 得到字節碼文件
Compiled from "NullpointerExceptionTest.java"
public class com.example.demo3.commonpitfalls.service.NullpointerExceptionTest {
public com.example.demo3.commonpitfalls.service.NullpointerExceptionTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: aconst_null
1: astore_1
2: aload_1
3: invokevirtual #2 // Method java/lang/Integer.intValue:()I
6: istore_2
7: return
}
可以發現,字節碼文件中有一個Integer.intValue() 這樣的調用,對象為null, 用null調用這個方法, 所以才會產生空指針異常。
這邊給出規避自動拆箱引發空指針的建議:
1、包裝類類型和基本數據類型都可以的業務場景下,優先考慮使用基本類型。
2、對于不確定的包裝類類型,一定要對NULL情況做檢驗和判斷。
3、對于值為NULL的包裝類類型,建議可以試著賦值為0;當然了,也要注意NULL和0代表的業務含義是否是一樣的。
2、字符串比較出現空指針異常
String str1 = "hello";
String str2 = null;
// 下面這行代碼會導致空指針異常,因為str2為null
int result = str1.compareTo(str2);
因為compareTo()方法會調用length(), 當傳入的對象為null, 會引發空指針異常。
3、并發容器規定不允許put null值時,強行put會引發空指針異常
如ConcurrentHashMap 這樣的容器不支持 Key 和 Value 為 null,強行 put null 的 Key 或 Value 會出現空指針異常。
ConcurrentHashMap map = new ConcurrentHashMap();
map.put(null, null); // 必然引發空指針異常, 因為多線程并發環境下,put(null,null)會引發二義性問題。
其實,規定鍵不能為null 以及 值不能為null, 是為了避免二義性的問題。null 是一個特殊的值,表示沒有對象或沒有引用。拿 get 方法取值來說,返回的結果為 null 會存在兩種情況:
值沒有在集合中;
值本身就是 null。
如果你用 null 作為鍵,那么就無法區分這個鍵是否是存在于 ConcurrentHashMap 中或者根本沒有這個鍵。同樣,如果你用 null 作為值,那么就無法區分這個值是真正存儲在 ConcurrentHashMap中的,還是因為找不到對應的鍵而返回了null。
另外,多線程環境下,無法使用containKey(key)來判斷是否存在這個key-value, 因為判斷的同時,會有其他線程對鍵值進行修改。
需要注意的是,HashMap 可以存儲 null 的 key 和 value,但 null 作為鍵也只能有一個,null 作為值可以有多個。
如果傳入null 作為參數,就會返回 hash 值為 0 的位置的值。單線程環境下,不存在一個線程操作該 HashMap 時,其他的線程將該 HashMap 修改的情況,所以可以通過 contains(key)來做判斷是否存在這個鍵值對,從而做相應的處理,也就不存在二義性問題。
4、對象級聯調用可能導致NPE
這種情況就是A類中包含了B類。有時候忘記對B類的實例進行判空,而導致NPE問題。
class B {
public void methodB() {
System.out.println("Method B called");
}
}
@Data
class A {
private B b;
}
public class Main {
public static void main(String[] args) {
B b = new B();
A a = new A(b);
// 沒有對字段進行空值檢查,直接嘗試級聯調用 B 對象的方法
a.getB().methodB(); // 這里可能會導致空指針異常
}
}
5、返回的List為null時,后調用了size(), 導致NPE
import java.util.List;
public class RemoteService {
public List<String> getData() {
// 模擬遠程服務返回的 List 為 null
return null;
}
}
public class Main {
public static void main(String[] args) {
RemoteService remoteService = new RemoteService();
List<String> data = remoteService.getData();
// 沒有對返回的 List 進行空指針檢查,直接調用方法可能導致空指針異常
int size = data.size(); // 這里可能導致空指針異常
System.out.println("Size of data: " + size);
}
}
以上是一些常見的可能會出現的空指針異常(NPE),下面通過一些實踐代碼來講一下如何去修復NPE問題。
其實,最常見的方法就是先判個空,然后再進行對象的操作,這樣可以直接避免NPE拋出。不過,這只能讓異常不再出現,我們最好是找到程序邏輯中出現的空指針究竟是來源于入參還是 Bug:
如果是來源于入參,應進一步分析入參是否合理等;
如果是來源于Bug,那空指針不一定是純粹的程序Bug,可能還涉及業務屬性和接口調用規范等。總而言之,結合實際業務代碼的上下文去修復問題。
如果時間有限,當然使用ifelse邏輯先判一下空是沒問題的。不過,java8提出的Optional 類是專門用來消除這樣的 if-else 邏輯,使用一行代碼就能進行判空和處理!
接下來,寫一個實踐的代碼,使用Optional類去修復上文提到的NPE問題。
實際例子
package com.example.demo3.commonpitfalls.service;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 分別用來測試java中常見的幾種場景下的NPE
* 1、參數值是 Integer 等包裝類型,使用時因為自動拆箱出現了空指針異常;
* 2、字符串比較出現空指針異常;
* 3、諸如 ConcurrentHashMap 這樣的容器不支持 Key 和 Value 為 null,強行 put null 的
* Key 或 Value 會出現空指針異常。
* 4、A 對象包含了 B,在通過 A 對象的字段獲得 B 之后,沒有對字段判空就級聯調用B的方法出現空指針異常。
* 5、方法或遠程服務返回的 List 不是空而是 null,沒有進行判空就直接調用 List 的方法出現空指針異常
*/
@Slf4j
@RestController
@RequestMapping("/nullpointer")
public class NullpointerExceptionTest {
/**
* 代碼中模擬了 5 種空指針異常
* 對入參 Integer i 進行 +1 操作;
* 對入參 String s 進行比較操作,判斷內容是否等于"OK";
* 對入參 String s 和入參 String t 進行比較操作,判斷兩者是否相等;
* 對 new 出來的 ConcurrentHashMap 進行 put 操作,Key 和 Value 都設置為 null。
* wrongMethod 的返回值, return null
*/
private List<String> wrongMethod(FooService fooService, Integer i, String s, String t) {
log.info("result {} {} {} {}",
i + 1,
s.equals("OK"), s.equals(t),
new ConcurrentHashMap<String, String>().put(null, null));
if (fooService.getBarService().bar().equals("OK"))
log.info("OK");
return null;
// 使用Optional類去做包裝,為null時會返回一個默認值,而不是返回null,從而避免拋異常。
private List<String> rightMethod(FooService fooService, Integer i, String s, String t) {
// i為null 時,i的值設置為0;i不為null時,返回i的值;再進行加1。 OK放在前面進行equals,就不會出現NPE。
log.info("result {} {} {} {}", Optional.ofNullable(i).orElse(0) + 1, "OK".equals(s), "OK".equals(t));
// 使用Optional包裝起來。如果不為null, 執行流的后續操作;如果為null, 返回空的Optional對象。
Optional.ofNullable(fooService)
.map(FooService::getBarService)
.filter(barService -> "OK".equals(barService.bar()))
.ifPresent(result -> log.info("OK"));
// 返回一個空的List, 比起返回null的好處是可以避免NPE。
return new ArrayList<>();
}
// 會拋NPE的錯誤使用
@GetMapping("wrong")
public int wrong(@RequestParam(value = "test", defaultValue = "1111") String test) {
return wrongMethod(
test.charAt(0) == '1' ? null : new FooService(),
test.charAt(1) == '1' ? null : 1,
test.charAt(2) == '1' ? null : "OK",
test.charAt(3) == '1' ? null : "OK").size();
}
// 不再拋NPE的正確使用
@GetMapping("right")
public int right(@RequestParam(value = "test", defaultValue = "1111") String test ) {
return Optional.ofNullable(rightMethod(
test.charAt(0) == '1' ? null : new FooService(),
test.charAt(1) == '1' ? null : 1,
test.charAt(2) == '1' ? null : "OK",
test.charAt(3) == '1' ? null : "OK"))
.orElse(Collections.emptyList()).size(); // 如果返回為null,則返回空列表。然后獲取空列表的size。
}
class FooService {
@Getter
private BarService barService;
}
class BarService {
String bar() {
return "OK";
}
}
}
測試結果
wrong接口

right接口


浙公網安備 33010602011771號