你被空指针折磨过吗?

1,273 阅读5分钟

作者爱说话

Hello,大家好,我是 行云

这是原创的第 11 篇文章,希望今天这篇文章能带给你一点思考和启发

上一周接受了一个任务,排期比较赶,由于开发数据库的数据问题,在调试的过程中,在我认为业务规则下数据不应该为空的地方,抛出的 NPE ,简直折磨死个人,并且使用的是公司自研的框架,不支持热部署,模块还巨多,每改动一次都得重新打包再启动,所以这一期就聊一聊这个大家耳熟能详的 NPE 到底是怎么回事,看下你是不是真的了解 NPE 并且都能完美规避它了。

你遇到过 NPE 吗?

空指针异常应该是开发者或多或少都接触过的 bug 之一

一不小心,就掉了这个坑里面,然后说声,对啊,忘记了,回到代码里,连忙加个判空操作

那么今天就来聊聊这个常见 NullPointerException 到底是个何方神圣

  • 透过现象看本质,到底什么是 NPE?
  • 就是不小心,哪些操作会导致 NPE ?
  • 我该怎么做,如何避免该死的 NPE ?

何为 NPE

空指针发生的原因是应用需要一个对象时却传入了 null,包含以下几种情况:

  • 调用 null 对象的实例方法

  • 访问或者修改 null 对象的属性

  • 获取值为 null 的数组的长度

  • 访问或者修改值为 null 的二维数组的列时

  • 把 null 当做 Throwable 对象抛出时

道理我都懂,那么在实际 Coding 中,哪些操作会引发 NPE 呢?

该死的 NPE,你又出现了

翻开我们熟悉的《手册》

手册中说:防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:

  1. 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE
  2. 数据库的查询结果可能为 null。
  3. 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null
  4. 远程调用返回对象时,一律要求进行空指针判断,防止 NPE
  5. 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针
  6. 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE
  7. Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals

接下来我们就看一些实际场景

林步动同学接受了一个包装类型的对象参数a ,然后经过一系列花里胡哨的操作,然后进行了 return 基本数据类型,但是如果传入的 参数 a 是 null 的话,sorry,空指针将会出现在你的控制台

public int test(Integer a)  {
    // something code
    return a;
}

林步动同学需要从数据库里查出一堆借据,然后看看是谁到底没还钱,但是数据库的查询结果可能为 null,一旦 loanReceiptDtoList 为 null,那么第一处就会出现 NPE

当然假设林步动同学查出了一些借据,可是,借据数据并不完整,一些借据数据没有客户名字,那么第二处就会报出 NPE

public void lookReceipt(Condition condition) {
        List<LoanReceiptDto> loanReceiptDtoList = loanReceiptMapper.sqlect(condition);
        for(LoanReceiptDto loanReceiptDto : loanReceiptDtoList) {
            // 第一处
            whoNoMoney(loanReceiptDto);
        }
    }

private void whoNoMoney(LoanReceiptDto loanReceiptDto) {
    // 第二处
    if (loanReceiptDto.getCustomerName().equals(小美)) {
        System.out.println("这是小美");
    };
}

林步动同接受小美同学传过来的 hashMap,但是他只简单判断了元素是否为 size 为0,但是假设小美传进来的是 ("k", null) ,取出来的值,依旧是 null

public void test(HashMap<String, String> hashMap) {
    // 这个结果是 true
    isNotEmpty(hashMap);
}

林步动同学想判断这个借据的客户名字是不是小美,这样写是对的吗? 当然不合适,正例应该是翻转equals的调用,小美应该作为方法 "equals()"的调用方,而不是参数。 你的调用方应使用常量或确定有值的对象来调用equals 当然 JDK7 引入的工具类 java.util.Objects#equals(Object a, Object b) 也是很香的

public void test() {
    LoanReceiptDto loanReceiptDto = new LoanReceiptDto();
    // 小美是魔法值,不建议哈,此处举例使用
    if (loanReceiptDto.getCustomerName().equals("小美")) {
        // something code
    }
}

接下来分享一个花式踩坑例子

// 我们作为使用方调用如下的二方服务接口
public Boolean someRemoteCall();

//  然后自以为对方肯定会返回 TRUE 或 FALSE,然后直接拿来作为判断条件或者转为基本类型,如果返回的是 null,则会报空指针异常:
if (someRemoteCall()) {
  // something code
 }

大家看例子的时候,可能觉得很简单,但是在实际开发的时候,往往会忽略一些场景去思考,此处是否会 NPE,希望大家且 Code 且小心

如何避免 NPE

(为了说明真实可用性,都尽量从项目代码中截图展示)

1,首推 使用 Optional

Optional 是 Java 8 引入的特性,返回一个 Optional 则明确告诉使用者结果可能为空:

2,其次 学习《代码简洁之道》第 7.8 节 中“别传 null 值”

也是最常见的方式之一,通过防御性参数检测,可以极大降低出错的概率,提高程序的健壮性

3,使用 lombok 的 @Nonnull 注解

4, 返回空集合

如果参数不合适直接返回空集合

5,使用 Objects

可以使用 Java 7 引入的 Objects 类,来简化判空抛出空指针的代码。

这边还可以使用一些工具包进行判断处理,不一一推荐了,希望大家可以关注 NPE 对程序健壮性带来的危害


我是 行云,点个赞再走吧,我们下期再见 👋