如何正确避免NullPointerException

NullPointerException | 基本功修炼

Posted by yyconstantine on August 25, 2020

如何正确避免NullPointerException


我们常说的代码健壮性,很多时候就是对各种异常情况的处理,比如数据的格式、数据的类型、非法输入、空指针等。前几种异常我们可以简单地通过JSR-303(Valid、Validation)来进行处理,而空指针是我们最常见、也最容易忽略的问题。作者也在实际开发中写出过很多NPE,痛定思痛决定水一篇博客出来记录一下。。

首先,空指针的场景我认为大致可以分为两种

  • 对象的空指针
  • 集合的空指针

一个基本认知

首先,对于我们后台开发的同学来说,一个最基本的认知就是,任何来源的数据我们都不能认为是可信的。即,我们获取到一个对象,马上想到它可能是一个null而非一个new出来的对象;我们获取到一个集合,马上想到它可能是一个null或者一个emptyList()。


对象的空指针

对象的空指针很好理解,我们在开发中会有很多的model,我们在Spring项目中会存在各种各样的bean,我们无法保证我们获取到的属性一定不为空,所以最基本的场景,我们会这样写:

if (obj == null) 
    return;

一般来说,作为一个前置的判断条件,我们这样处理就足够了,但是仍然有一些特殊场景,可能我们需要用到一个非空的属性进行某些操作(允许其属性为空),则我们可以这样写:

Object nonNullObj = Optional.ofNullable(obj).orElse(new Object());

这样我们可以正常地使用包装过的obj而不必担心NPE问题

即使如此,也还是存在NPE的情况,假设我们定义一个model如下:

@Data
public class Model {
    private Integer number;
}

在我们基于上述场景使用obj的时候,进行这样的操作:

if (nonNullObj.getNumber().equals(100)) {
    // do something
}

由于我们的number属性没有初始化,则会抛出NPE。个人觉得有两种避免方式:

  • 使用equals进行比较,尽量将已知的量放在括号外进行比较:

    if (100.equals(nonNullObj.getNumber())) {
        // do something
    }
    
  • 或者使用Optional的特性(看起来有些啰嗦):

    if (Optional.ofNullable(obj)
        .filter(Objects::nonNull)
        .filter(o -> o.getNumber() != null && o.getNumber().equals(100))
        .isPresent()) {
        // do something
    }
    

集合的空指针

当你避开了对象的空指针,恭喜你过了一关,但是集合的空指针更是让人防不胜防~(踩坑无数)

如最开始所说,我们仍然对一个传递过来的集合进行非空判断:

if (CollectionUtils.isEmpty(list)) 
    return;
if (MapUtils.isEmpty(map)) 
    return;

一个值得注意的点是,除了进行非空判断,我们应当尽量自己定义一个集合去接受数据,而不是直接操作获取的数据集合:

ArrayList<Integer> myList = dataSource.list();

这样做的原因是:我们接收到的集合类,可能其并不是一个完整的List/Map。

为什么这么说呢?我们翻看ArrayList的源码知道,其继承了一个AbstractList,这个抽象List已经实现了一部分方法,但是如add方法并没有实现,而是抛出了一个UnsupportedOperationException,Map也同理。

另外,如果我们只是简单地循环一个接收的集合,可能并不需要再单独定义一个变量接收,则我们也可以这样写:

Optional.ofNullable(list).orElse(new ArrayList())
    .forEach(System.out::println);

此外,我们考虑到,可能集合中存在部分元素为空,则我们对上述代码进行增强:

Optional.ofNullable(list).orElse(new ArrayList())
    .stream()
    .filter(Objects::nonNull)
    .forEach(System.out::println);

另外,我们可能不单单是进行一些简单的循环操作,在stream操作中我们经常会使用map进行模型转换,这里同样存在一个坑:

Map<Integer, String> map = Maps.newHashMap();
List<Integer> list = Lists.newArrayList(1, 2, 3);
List<String> newList = list.stream()
    .filter(Objects::nonNull)
    .map(item -> map.get(item))
    .collect(Collectors.toList());

我们可以看到,map只进行了初始化,实际没有赋值,所以我们每次循环的map.get(item)都取到null值,但是collect会将这些null值作为集合的元素全部填充到集合内,这时我们再使用这个集合就会出现问题,尽管我们进行了常规的判空操作:

if (CollectionUtils.isEmpty(newList)) {
    return;
}

System.out.print(newList.get(0));

在我们使用get(0)时,发生NPE,原因是我们的newList集合元素为:[null, null, null]

所以,我们对进行了模型转换操作的后,需要再次进行非空过滤:

List<String> newList = list.stream()
    .filter(Objects::nonNull)
    .map(item -> map.get(item))
    .filter(Objects::nonNull)
    .collect(Collectors.toList());