Java三元表示式裝箱拆箱NPE問題
問題
今天在測試環境的運營後臺查詢商品庫存時發現後端介面報錯,返回code為904,該錯誤碼錶示內部錯誤。於是在微服務日誌裡檢視,發現某方法報了NPE(java.lang.NullPointer)。
方法裡關鍵的報錯程式碼如下:
public Integer queryXxx(String xx, String yy) {
...
XxxRo xxxRo = queryXxxRo(xx, yy);
return xxxRo != null ? xxxRo.getQuantity() : 0;
}
日誌裡錯誤異常堆疊裡看到拋NPE異常的行號對應這一行程式碼:
return xxxRo != null ? xxxRo.getQuantity() : 0;
初探
初一看如果變數xxxRo
為null,那麼xxxRo.getQuantity()
會拋NPE。
可語句裡判斷了的xxxRo
不為null才執行,否則返回0,按理說變數xxxRo
為null應返回0。
queryXxxRo(xx, yy)
是從Redis裡查詢資料,將相關引數拼好key在Redis去查發現有資料。
Redis裡儲存型別為hash,對應XxxRo裡的每個欄位,其中hget xxx quantity
值為4000000012。
復現
本地啟動庫存服務,通過dubbo支援的telnet裡invoke
命令呼叫該介面,也是那一行程式碼拋NPE。
我在return xxxRo != null ? xxxRo.getQuantity() : 0;
發現
xxxRo
不為null,在IDEA裡展開該物件,其中各欄位都有值,只有quantity
欄位為null。通過Fn + Option + F8調出Evalute視窗,將
xxxRo != null ? xxxRo.getQuantity() : 0
複製進去執行,結果為null,並沒有拋NPE。F9放開斷點進行執行,日誌裡列印NPE,跟測試環境一致。
分析
仔細審視這行程式碼,它用到了三元表示式來判斷,表示式執行後直接return,而方法的返回是Integer型別,
而xxxRo.getQuantity()
也是Integer型別,好像沒問題。
注意到三元表示式的另一個分支返回的是0,想起Java在裝箱/拆箱時(boxing/unboxing)可能會有NPE,
剛才本地復現時通過斷點在IDEA的看到xxxRo的quantity
可能這裡先進行了拆箱,然後進行裝箱的轉換。
模擬
int b = (Integer) null;
這行程式碼null經過裝箱,然後自動拆箱時拋了NPE
繼續通過幾個例子來模擬:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Optional;
/**
* @author cdfive
*/
public class SpecialNPETest {
public static void main(String[] args) {
// Basic test
basic();
// OK
case1();
// NPE
case2();
// NPE
case3();
// OK
case4();
// OK
case5();
// OK
case6();
}
private static void basic() {
System.out.println("basic start");
// OK
Integer a = (Integer) null;
// NPE
try {
int b = (Integer) null;
} catch (Exception e) {
System.out.println("exception=" + e.getClass().getName() + ",msg=" + e.getMessage());
}
System.out.println();
System.out.println("basic end");
}
private static void case1() {
System.out.println("case1 start");
try {
Item item = new Item();
item.setId(1);
item.setQuantity(5);
// OK
Integer result = item != null ? item.getQuantity() : 0;
System.out.println(result);
} catch (Exception e) {
System.out.println("case1 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage());
}
System.out.println("case1 end");
System.out.println();
}
private static void case2() {
System.out.println("case2 start");
try {
Item item = new Item();
item.setId(1);
// NPE
Integer result = item != null ? item.getQuantity() : 0;
} catch (Exception e) {
System.out.println("case2 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage());
}
System.out.println("case2 end");
System.out.println();
}
private static void case3() {
System.out.println("case3 start");
try {
Item item = new Item();
item.setId(1);
// NPE
int result = item != null ? item.getQuantity() : 0;
System.out.println(result);
} catch (Exception e) {
System.out.println("case3 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage());
}
System.out.println("case3 end");
System.out.println();
}
private static void case4() {
System.out.println("case4 start");
try {
Item item = new Item();
item.setId(1);
// OK
Integer result;
if (item != null) {
result = item.getQuantity();
} else {
result = 0;
}
System.out.println(result);
} catch (Exception e) {
System.out.println("case4 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage());
}
System.out.println("case4 end");
System.out.println();
}
private static void case5() {
System.out.println("case5 start");
try {
Item item = new Item();
item.setId(1);
// OK
Integer result = item != null ? item.getQuantity() : Integer.valueOf(0);
System.out.println(result);
} catch (Exception e) {
System.out.println("case5 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage());
}
System.out.println("case5 end");
System.out.println();
}
private static void case6() {
System.out.println("case6 start");
try {
Item item = new Item();
item.setId(1);
// OK
Integer result = Optional.ofNullable(item).map(o -> o.getQuantity()).orElse(null);
System.out.println(result);
} catch (Exception e) {
System.out.println("case6 error,exception=" + e.getClass().getName() + ",msg=" + e.getMessage());
}
System.out.println("case6 end");
System.out.println();
}
@NoArgsConstructor
@AllArgsConstructor
@Data
private static class Item implements Serializable {
private Integer id;
private Integer quantity;
}
}
解決
return xxxRo != null ? xxxRo.getQuantity() : 0;
修改這行程式碼。
3種思路:
- 改用if/else判斷
Integer result;
if (xxxRo != null) {
result = xxxRo.getQuantity();
} else {
result = 0;
}
- 基礎型別0改為包裝型別Integer.value(0)
Integer result = xxxRo != null ? xxxRo.getQuantity() : Integer.valueOf(0);
- 改用Optional處理
Integer result = Optional.ofNullable(xxxRo).map(o -> o.getQuantity()).orElse(0);
注意:
- 當xxxRo不為null,xxxRo裡的quantity為null時,前2種方式返回的是null,第3種方式裡返回的是0
- 當xxxRo為null時,3中方式都返回0
思考
剛才有個點忽略了,通過hget xxx quantity
在Redis查出來值為4000000012,為何xxxRo裡的quantity欄位為null?
注意到日誌裡還有一個異常:
java.lang.NumberFormatException: For input string: "4000000012"
這裡因為4000000012超過了Integer.MAX
的值2147483647,專案框架裡Redis的hash轉換為Ro物件時用的Integer.valueOf()
,
該方法拋的NumberFormatException
,轉換單個欄位失敗後記錄了錯誤日誌並繼續執行。
4000000012是其它系統推過來的值,經檢查日誌和溝通,是測試同學另外一個系統的介面上設定的值過大。