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。

quantity在Redis查出来值为4000000012,为何xxxRo里的quantity字段为null?
注意到日志里还有一个异常:
java.lang.NumberFormatException: For input string: "4000000012"
这里因为4000000012超过了Integer.MAX的值2147483647,项目框架里Redis的hash转换为Ro对象时用的Integer.valueOf()
该方法抛的NumberFormatException,转换单个字段失败后记录了错误日志并继续执行。
4000000012是其它系统推过来的值,经检查日志和沟通,是测试同学另外一个系统的界面上设置的值过大。

通过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
posted @ 2021-10-21 20:53  cdfive  阅读(390)  评论(0编辑  收藏  举报