谢谢你留下时光匆匆
如何避免 Java NPE(NullPointerException) 空指针问题

NPE(NullPointerException) 是在Java开发中常遇到的问题,特别对于刚入门的Java开发者来说,很容易忽略空指针的问题,进而影响整理代码质量。自己查阅了网络上相关资料,在这里对NPE空指针问题的解决方法做一个总结。

NPE的应对,整体上分为两大情况——1,接受外部数据,处理这些外部数据时,出现NPE;2,自己定义方法/接口时,空指针作为返回结果。

接受外部数据时避免出现NPE

有时候我们会将自己的代码封装成包或者服务,以供他人调用。外界调用我们代码时候传入的参数可能会不符合我们的预期,此时如果我们不对其进行检验,就有可能出现NPE的问题:例如,传入参数为空值,或者传入的请求没有某个字段。这种情况下,我们务必做好入参合法性检验,避免NPE报错的出现。以下是外部数据空指针检查处理的一些经验。

对方法入参进行 null 空值判断以及报错

防止 null 值入参对我们代码程序的影响,最直接的方式就是对入参进行 null 判断,对不符合预期的 null 值进行特殊处理(抛出异常Exception,输出相应日志等)。

我们可以使用Objects.requireNonNull(x) 进行空值检查,当参数为 null,则会抛出异常。我们也可以在方法声明时,在参数前加入@NotNull修饰符,例如public void foo(@NotNull int x)。这样在进行开发时,IdeaJ 会对明显的空值输入提示,从而减少NPE发生可能。进一步,我们可以使用 lombok 包中的 @NonNull 修饰符同时完成上述两点,和 @NotNull 一样用法,在编译时 lombok 会在方法开头自动生成空值检测的代码,此外 IdeaJ 也会有相应的入参空值提醒。

需要注意的是,我们的代码作为模块被别人开发调用时,入参空值报错是可行的,但是不是所有情况抛出异常都是合适的,如果我们的代码是线上作为服务被别人调用,我们可以进行日志输出和相应的空值逻辑处理,而非简单的抛出异常,以保证我们线上服务状态的正常。

对get方法返回的值多加小心

常常我们需要从某个数据对象获取某个字段值,比如从一个pojo类中get某个字段,从map中拿到key对应的value,或者是从上游请求传来的jsonobject中获取某个字段值。有时候疏忽了这些数据对象隐含的null值可能性,会导致NPE问题。例如,Map类的get方法在key不存在时,会返回null;同样的,fastjson中JSONObect类的get相关方法(e.g. getString, getArray)也会在给定字段不存在时返回null。

一个简单的经验准则,当方法名有get时,有意识考虑这个get出来的值会不会是空值null,会不会导致空指针的问题。

判断null

直接判断null的写法

在进行 null 判断的时候,我经常看到这样的写法 if(null == yourObj),为什么要把null写在判断表达式的前方?我在网上查阅了一下相关资料,这样写的原因主要是防止在 if 语句中相等判断运算符==误写成赋值运算符=,在Java 1.5以前,如果将if(yourObj == null)写成 if(yourObj = null)在编译时,是不会报错的。虽然在Java 1.5以后 if语句要求表达式值为布尔 boolean 值,但if(null == yourObj)的代码习惯一直延续下来。

此外,如果你对代码可读性要求更高,我们可以用 Objects.nonNull()(或者Objects.isNull())来判断一个对象是否为空值。

连续判断null情况下一种简洁写法

在解析json时,如果要取的字段是多层级嵌套在里层的,会遇到连续判断空指针的情况,如果在if里面嵌套if语句会影响代码可读性,一种办法是使用Optional解决这个问题。具体的例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// {
//     "id": 1,
//     "name": "Adam",
//     "info": {
//         "height": 180,
//         "weight": 75
//     }
// }

// {
//     "id": 2,
//     "name": "Bob"
// }

// {
//     "id": 3,
//     "name": "David",
//     "info": {
//         "weight": 70
//     }
// }

adam.getInfo().getHeight() // 180
bob.getInfo().getHeight()  // throw NPE,因为info为null
david.getInfo().getHeight() // null

//Optional.of(180)
Optional.ofNullable(adam.getInfo()).map(Person::getInfo).map(Info::getHeight)

//Optional.empty  即使 info 为空值null,也不会报错
Optional.ofNullable(bob.getInfo()).map(Person::getInfo).map(Info::getHeight)

//Optional.empty  最后的 null 也会转换成 Optional.empty,这也可以避免下游使用 height 时候潜在的 NPE 报错
Optional.ofNullable(david.getInfo()).map(Person::getInfo).map(Info::getHeight)

一些空值判断的 Helper 方法

当我们在判断字符串类型 String 变量为空时候,有时候null值和空字符串“”会用相同的业务逻辑去处理,我们可以利用 org.apache.commons.lang3 中辅助类StringUtilsisEmpty()进行字符串为“空”的判断,该方法在字符串为null值或空字符串时都会返回true。更进一步的,StringUtilsisBlank()在上面两种情况外,在字符串只有空格的情况也返回true。

类似的,在判断集合类为空时候,null值与空集合可能有相同的处理逻辑,我们也可以利用org.apache.commons.lang3 中辅助类 CollectionUtilsisEmpty() 同时判断变量是否为null值或者空元素集合。

这里附上org.apache.commons.lang3 类的pom引用片段,方便大家参考

1
2
3
4
5
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

处理字符串时一些避免NPE的技巧

在进行字符串String相关操作时候,有两个简单的技巧可能避免潜在的空指针问题。

一是,在字符串比较时,如果是变量和一个常量比较,使用常量的equals方法,例如:

1
2
3
4
5
6
7
8
9
// 如果 name 为 null,将会报错
if (name.equals("Bob")) {
		...
}

// 可以避免 name 为 null 的情况。通常业务开发情形下,我们不会认为null与某个常量是相等的。
if"Bob".equals(name)) {
		...
}

如果是两个String变量比较,而且这两个变量都有可能为null值,不论使用哪个变量的equals()方法都有可能造成潜在的NPE错误。这种情况下,我们可以使用Objects.equals(x, y)方法进行值相等的判断。Objects.equals方法有一个地方需要特别注意,如果进行比较的两个变量都为null,会返回true,该返回的true值是否符合逻辑,需要在业务场景下判断。

二是,将其它类型变量转换成String时,用 String.valueOf(foo) 而非 foo.toString()

1
2
3
name.toString()  // 如果 name 为 null,会报NPE错误

String.valueOf(name)   // 如果 name 为 null,不会报NPE错误,会返回字符串 "null"

自己定义方法时 null 的返回

NPE出现的根本原因是开发者忽略了潜在的空值,那么我们在写自己方法时候,就尽量避免返回null值,从源头上解决NPE问题的出现。

用其它表示空的值替代null

首先,我们可以考虑一下自己所写的方法是否真的有必要返回null值,方法返回null值的意义是什么,是否可以用其它表示“空”含义的值替换null。例如,如果方法声明返回的是一个List,我们可以考虑返回空List Collections.emptyList() 而非null;同样的,如果方法要返回的是String,我们也可以考虑使用空字符""来替代null。

null无法被替换的情况

当然,并非一味替换null就是合适的,我们需要结合具体业务场景,例如,当你去解析外部服务的请求结果,并返回一个List时,可能你需要用null去表达服务调用失败,来避免混淆实际返回结果为空List的情况。

在这些业务场景下,null值有着无法被替代的意义,返回null可能是最好的解决方案,这时也有一些方法可以避免潜在的NPE错误。

  1. 返回 Optional 显式地声明可能存在的空值。将方法要返回null的地方改为返回 Optional.empty()。当自己或其他开发者在调用该方法时,会被强制要求考虑返回值可能为空的情况,从而避免NPE的出现。具体关于Optional的介绍,可以参考https://www.baeldung.com/java-optional。
  2. 在可能返回空值的方法上方加入 @Nullable 修饰,这样IDEA会在该方法调用处提醒处理可能的空值。
1
2
3
4
@Nullable
public static String foo (String x) {
    return null;
}
  1. 使用空对象设计模式,这个方法可能不太常用,这里不作赘述,具体可以参考 https://www.runoob.com/design-pattern/null-object-pattern.html

小结

  1. 接收外部数据时候,需意识到潜在null值的出现。方法名有get时候,考虑到返回值可能为null
  2. 字符串操作时,foo.equals("some string")替换为 "some string".equals(foo)foo.toString() 替换为 String.valueOf(foo)
  3. 可以利用Optional.ofNullable(...).map(...).map(...).orElse()的写法处理多层级嵌套时,get中出现null的情况
  4. 开发新方法时,如果返回List,可以考虑返回空列表Collections.emptyList()来替代返回null
  5. 开发新方法时,如果返回String,可以考虑返回空字符串来替代返回null
  6. 开发新方法时,如果确实需要返回null,可以考虑使用Optional显式声明空值
  7. StringUtils.isEmpty() 可以同时判断字符串为null或空字符串“”的情况,StringUtils.isBlank() 还可以判断字符串只包含空格的情况
  8. CollectionUtils.isEmpty() 可以判断一个集合为null或空元素集合的情况

参考资料