简单谈谈空指针

技术分享PPT下载:简单谈谈空指针.pptx

0. NPE

Java中,null是一个关键字,用来标识一个不确定的对象。因此可以将null赋给引用类型变量,但不可以将null赋给基本类型变量。

NullPointerException是常见的错误,直接抛出异常也是经常使用的处理方式,也就是“快速失败”,避免更大的问题。当然这就将异常的处理转移给了调用方。

这里不谈抛异常,谈一谈NPE的预防以及怎样处理可能的NPE。

1. 从引用说起

引用的结构

我们访问Java对象需要通过栈上的reference来操作堆上的具体对象。HotSpot虚拟机使用的是直接指针方式访问,在reference中存储的是堆中对象地址,而Java堆对象除了存储自身数据外,还要存储类型数据的地址等相关信息(类型相关信息存储在方法区)。

来几个更具体的图示:

基本类型引用

未初始化引用(null),eg:String s或者String s = null;

初始化的引用,eg:String s = new String(“iQiyi”);

JVM如何知道何时抛出NullPointerException呢?
  1. 声明一个未初始化的null变量,即执行指令aconst_null, 将一个null引用压入栈

  2. 在调用某一方法之前会显式检查(test explicitly)是否为null,但JIT会做很多优化,避免每次都检查,如

    1. 如果执行this上的方法,则不会产生NPE
    2. 如果经过一次调用没有产生NPE,那么再次调用仍不会

    注:以上均是指同一个栈帧环境下

2. 易出现空指针的地方

a. 自动拆箱(编译期)引发的空指针

1
2
Integer amount = ...; // 有可能是null
int sum = amount; // 实际为int sum = amount.intValue();

b. 无意识空指针

此类大部分都是未检查直接调用导致

1
2
3
4
String s = ...; // 有可能是null
int length = s.length();
boolean equals = s.equals("Another String");
...

c. 将null赋值给基本类型(框架反射调用等)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class NullProperty {
private int id;
public static void main(String[] args) {
NullProperty nullProperty = new NullProperty();
Field idField = ReflectionUtils.findField(NullProperty.class, "id");
boolean isPrimitive = idField.getType().isPrimitive(); // true, 表示该field为基本类型
ReflectionUtils.makeAccessible(idField);
ReflectionUtils.setField(idField, nullProperty, 1); // success
ReflectionUtils.setField(idField, nullProperty, null); // 基本类型赋值null,则会throw IllegalArgumentException
}
}

举个例子:数据库某字段为NULL,且对应数据模型的属性为基本类型。此时会抛出如下异常

可以清楚的看到:Null值被赋值给基本类型的属性。基本类型默认值与数据库中null值表达的意思可能并不相同,故大多数框架会抛异常

建议:

  1. 数据库字段设置非空,尤其是数字型字段要非空有默认值;
  2. 对应模型的数字型字段使用包装类型,而非基本类型

3. 推荐的实践

a. 参数检查,快速失败(fail-fast)

除了使用显式入参校验外,还可以使用Bean Validation(JSR 303),提供了包括空指针检查(@NotNull)在内的丰富参数校验功能

b. 非空对象在前,执行.equals等方法

1
2
3
4
String name = ...;
if ("iQiyi".equalsIgnoreCase(name)) {
// do sth
}

c. 【空安全方法调用】使用Apache Commons的一些工具,如StringUilts,里面有很多避免手动检查null值的方法

1
2
3
4
5
6
7
8
9
10
// strA, strB均有可能为null
String strA = ...;
String strB = ...;
StringUtils.equals(strA, strB);
StringUtils.isBlank(strA);
StringUtils.isNotEmpty(strA);
StringUtils.defaultIfBlank(strA, strB);
CollectionUtils.isNotEmpty(list)
...

还有很多类似的工具包,常用的还有处理集合的工具CollectionUtils。Apache、Guava等都有类似产品。

d. 方法返回时,避免返回null。返回空集合、数组或专用的空对象NullObject

1
2
3
4
Collections.emptyList();
Collections.emptySet();
Collections.emptyMap();
NullObject

注:返回的集合均不支持修改操作。

e. 关于基本数据类型与包装数据类型的推荐使用方法:

  1. 所有的 POJO 类属性必须使用包装数据类型。
  2. RPC 方法的返回值和参数必须使用包装数据类型。
  3. 所有的局部变量【推荐】使用基本数据类型。

正例:数据库的查询结果可能是 null,因为自动拆箱,用基本数据类型接收有 NPE 风险。
反例:比如显示大气温度,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示:0℃,这是不合理的,应该显示成中划线-。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。

f. Map 类集合 K/V 能不能存储 null 值的情况:

集合类 Key Value Super 说明
HashTable 不允许null 不允许null Dictionary 线程安全
ConcurrentHashMap 不允许null 不允许null AbstractMap 分段锁技术
TreeMap 不允许null 允许为null AbstractMap 线程不安全
HashMap 允许为null 允许为null AbstractMap 线程不安全

注意区分ConcurrentHashMap与HashMap,这两个比较常用。由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,注意存储
null 值时会抛出 NPE 。

小结:
  1. 方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题。
    说明:防止 NPE 是调用者的责任。

  2. NPE 产生的场景:

    a. 返回类型为包装数据类型,有可能是 null,返回 int 值时注意判空。
    反例:public int f(){ return Integer 对象}; 如果为 null,自动解箱抛 NPE。

    b. 数据库的查询结果可能为 null。

    c. 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null,需要二次判断。

    d. 远程调用返回对象,一律要求进行 NPE 判断。

    e. 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。

    f. 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。

下面讲级联调用的解决办法。

4. Optional与Lambda

一句话介绍Optional

A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value.

from Java Doc

这是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回容器持有的对象。

a. 工厂方法创建一个Optional对象

1
2
3
4
// 创建对象时传入的参数不能为null
Optional<String> name = Optional.of("iQiyi");
// 创建了一个不包含任何值的Optional实例
Optional empty = Optional.ofNullable(null);

b. 其他诸如isPresent、get、ifPresent、orElse、orElseGet、orElseThrow等方法不再赘述,可以阅读相关介绍

c. Optional处理Null的典型场景

来看这样一个对象,有可能是JavaBean,也有可能是Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"code": "A00000",
"msg": "处理成功",
"data": {
"user": {
"id": 123,
"name": "John"
},
"company": {
"id": 10086,
"name": "CMCC"
}
}
}

如果业务需要data.user.name节点的数据,则需要通过:

1
2
3
4
5
6
7
8
9
10
11
12
13
String userName = null;
if (response != null) {
DataBean data = response.getData();
if (data != null) {
UserBean user = data.getUser();
if (user != null) {
userName = user.getName();
}
}
}
if (userName == null) {
userName = ""; // Default Value
}

or(暴力出奇迹!)

1
2
3
4
5
try {
userName = response.getData().getUser().getName();
} catch (Exception e) {
userName = ""; // Default Value
}

总是不如Groovy等一些编程语言自带的安全的属性/方法访问操作符方便。

1
2
// 任意一个调用返回null,则返回null,否则返回具体值。不会抛异常
String name = response?.getData()?.getUser()?.getName();

使用Optional处理NullPointerException,也可以达到类似的效果:

JavaBean形式:

1
2
3
4
String userName = resOptional.map(Response::getData)
.map(DataBean::getUser)
.map(UserBean::getName)
.orElse(""); // Default Value

如果是Map形式,稍显繁琐:

1
2
3
4
5
6
Object res = resOptional.map(o -> o.get("data"))
.map(
o -> ((Map) o).get("user")
).map(
o -> ((Map) o).get("id")
).orElse(""); // Default Value

这里也多说一句,JDK的升级很多都是借鉴Guava里面的思想来进行的。Optional首先是在Guava里有实现,后被JDK借鉴。

5. 参考资料

NullPointerException and the best way to deal with it

https://yq.aliyun.com/articles/69327?utm_content=m_10088

Optional

https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/