Menu

Java NullPointerException:原因与解决方法

Java 中的 NullPointerException 到底是什么意思,引发它的常见方式,如何读懂错误信息,以及预防它的写法。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

最常见的 Java 错误

当你把一个什么都不指向的引用(也就是 null)当作指向真实对象来使用时,就会发生 NullPointerException(大家都叫它 NPE)。对象类型的 Java 变量要么存放一个对象,要么存放 null,即“这里没有对象”的值。一旦你让 null 去做点什么,比如调用它的方法、读取它的字段、按索引访问它,那里根本没有可以操作的东西,JVM 就会抛出异常。

上一页的 try-catch 是用来处理预料之中的失败的,而 NPE 几乎总是一个单纯的 bug。这一页的目标不是捕获它们,而是弄懂它们为什么会发生,并写出不会产生它们的代码。

length() 没有可以作用的 String,所以程序因 NullPointerException 而终止。

读懂错误信息

从 Java 14 开始,错误信息会准确告诉你哪个东西是 null,这被称为 Helpful NullPointerException。在修改任何代码之前,先把它读一遍:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
	at Main.main(Main.java:4)

有两部分很关键。Cannot invoke "String.length()" 是失败的操作,because "name" is null 指出了罪魁祸首。at Main.main(Main.java:4) 这一行是堆栈跟踪,指向出错的具体行。所以正确的修复不是“把第 4 行包进 try”,而是“弄清楚第 4 行的 name 为什么是 null”。bug 几乎总是在更早的地方:那里本应给这个值赋值,却没有赋。

引发 NPE 的几种方式

NPE 来自少数几种操作,它们都是“碰到了 null”的不同形式:

Map 查找是一个典型来源:键不存在时 get 返回 null,而 NPE 往往在几行之后你真正使用这个值时才冒出来。拆箱则很隐蔽:把 null 的 Integer 赋给 int 会抛出异常,因为没有可以复制的数字。

用 null 检查来防护

最简单的防御是在使用引用之前,用 if 确认它不是 null:

当你把变量和一个 String 常量比较时,把常量放在前面:写 "yes".equals(answer),而不是 answer.equals("yes")。如果 answer 是 null,第一种写法会平静地返回 false,第二种则会抛出异常。

用 Objects.requireNonNull 快速失败

到处散落 null 检查会让代码很杂乱。当一个值永远不应该是 null 时,比如构造方法的参数,就在入口处用 Objects.requireNonNull 校验它。它会在坏值到来的那一刻立即抛出异常,并附上清晰的信息,而不是等到之后在代码深处才出问题:

这种“快速失败”的习惯,能把 200 行之外一个含糊的 NPE 变成源头处一条精确的报错。这里捕获异常只是为了展示错误信息;在真实代码中,你应该让它抛出来,这样 bug 才会被修复。

从一开始就避免 null

最好的 NPE 是永远不会发生的 NPE,因为根本没有可以碰到的 null。几个习惯就能起很大作用:

  • 返回空集合或空字符串,永远不要返回 null。Collections.emptyList() 和 "" 都可以放心地遍历和调用方法。
  • 对 Map 使用 getOrDefault,这样查找没有命中时也能得到一个真实的值,而不是 null。
  • 在声明字段时就初始化,不要让它们一直是 null,等到“以后再说”。

当一个值确实是可选的,比如一次查找可能合理地什么都找不到,Java 提供了 Optional:这个容器会强制调用者处理“值不存在”的情况,而不是悄悄交回一个 null。如果你想在 API 设计中彻底消除这些空缺,这就是接下来要读的相关概念。

总结

NullPointerException 是 Java 在告诉你:你把一个存放着 null 的引用当成存放着对象来用了。解决办法很少是捕获它,而是读懂详细的错误信息,追溯到本应给这个值赋值的地方,然后要么保证它不为 null,要么在使用它的地方加以防护。在入口处依靠 Objects.requireNonNull 快速失败,优先使用空值和 getOrDefault 而不是 null,当“不存在”是真实且预料之中的结果时,就用 Optional。掌握了这种思路,Java 中最常见的错误就会成为你自己代码里最少见的错误之一。

常见问题

Java 中 NullPointerException 是怎么引起的?

当你把一个指向 null 的引用当作指向真实对象来使用时,就会发生这种异常,比如调用方法(name.length())、读取字段、访问数组元素,或者对一个 null 的 Integer 拆箱。变量里没有对象,也就没有可以操作的东西,于是 JVM 抛出 NullPointerException。

Java 中如何解决 NullPointerException?

先读错误信息:从 Java 14 开始,它会准确指出哪个东西是 null(例如 "Cannot invoke "String.length()" because "name" is null")。然后追查这个变量为什么是 null:漏了初始化、某个方法返回了 null,或者 Map 查找没有命中。从源头修复,让这个值永远不为 null,或者在使用处用 null 检查、Objects.requireNonNull 或 Optional 加以保护。

检查 null 好,还是捕获 NullPointerException 好?

检查 null。NullPointerException 意味着你的逻辑有 bug,而不是一种预料之中的情况,所以应该预防它,而不是捕获它。捕获它只会掩盖真正的问题所在。把 try/catch 留给真正的异常情况,比如 I/O 失败。

Coddy 编程语言插图

和 Coddy 一起学编程

开始