1. 程式人生 > 其它 >Java三元表示式裝箱拆箱NPE問題

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

欄位為null,因為返回值是0,
可能這裡先進行了拆箱,然後進行裝箱的轉換。

模擬

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種思路:

  1. 改用if/else判斷
Integer result;
if (xxxRo != null) {
    result = xxxRo.getQuantity();
} else {
    result = 0;
}
  1. 基礎型別0改為包裝型別Integer.value(0)
Integer result = xxxRo != null ? xxxRo.getQuantity() : Integer.valueOf(0);
  1. 改用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是其它系統推過來的值,經檢查日誌和溝通,是測試同學另外一個系統的介面上設定的值過大。