问题:什么原因导致 NullPointerException
(NPE)?
您应该知道,Java 类型分为 原始类型 ( boolean
, int
等)和 引用类型 。Java 中的引用类型允许您使用特殊值 null
,这是 Java 表示“无对象”的方式。
当作真实引用 NullPointerException
使用时,运行时都会抛出 null
A。
public class Test {
public static void main(String[] args) {
String foo = null;
int length = foo.length(); // HERE
}
}
标记为 \'HERE\' 的语句将尝试 length()
上运行该方法 null
,这将引发 NullPointerException
.
有很多方法可以使用一个 null
值来产生一个结果 NullPointerException
。事实上,你 可以 用一个值来做 null
不会导致 NPE 的事情只有以下几种:
-
将其分配给引用变量或从引用变量读取,
-
将其分配给数组元素或从数组元素中读取它(前提是数组引用本身非空!),
-
将其作为参数传递或作为结果返回,或者
-
或
==
进行测试 !=
,或者 instanceof
.
问题:如何读取 NPE 堆栈跟踪?
假设我编译并运行上述程序:
$ javac Test.java
$ java Test
Exception in thread "main" java.lang.NullPointerException
at Test.main(Test.java:4)
$
第一个观察结果:编译成功!程序中的问题不是编译错误。这是 运行时 错误。(某些 IDE 可能会警告您的程序将始终抛出异常……但标准 javac
编译器不会。)
第二个观察结果:当我运行该程序时,它输出两行“胡言乱语”。 错! 那不是胡言乱语。这是一个堆栈跟踪……如果您花时间仔细阅读,它提供了 重要信息 ,可帮助您追踪代码中的错误。
让我们看看它说了什么:
Exception in thread "main" java.lang.NullPointerException
堆栈跟踪的第一行告诉您许多事情:
-
它告诉您抛出异常的 Java 线程的名称。对于只有一个线程的简单程序(如本程序),它将是 \'main\'。让我们继续...
-
它告诉您引发的异常的全名;即
java.lang.NullPointerException
.
-
如果异常具有相关的错误消息,则会在异常名称后输出。
NullPointerException
在这方面是不寻常的,因为它很少有错误消息。
第二行是诊断 NPE 最重要的一行。
at Test.main(Test.java:4)
这告诉我们很多事情:
-
\'at Test.main\' 表示我们处于
main
该类的方法 Test
。
-
\'Test.java:4\' 给出了该类的源文件名,并且它告诉我们发生此情况的语句位于文件的第 4 行。
如果您计算上面文件中的行数,第 4 行就是我用 \'HERE\' 注释标记的行。
请注意,在更复杂的示例中,NPE 堆栈跟踪中会有很多行。但您可以确定第二行(第一个 \'at\' 行)会告诉您 NPE 被抛出的位置1.
简而言之,堆栈跟踪将明确地告诉我们程序的哪个语句抛出了 NPE。
另请参阅: 什么是堆栈跟踪,以及如何使用它来调试我的应用程序错误?
1 - 不完全正确。有一种东西叫做嵌套异常...
问题:如何在我的代码中追踪 NPE 异常的原因?
这是最难的部分。简而言之,就是对堆栈跟踪、源代码和相关 API 文档提供的证据进行逻辑推理。
我们先用上面的简单示例来说明一下。我们首先查看堆栈跟踪告诉我们发生 NPE 的位置:
int length = foo.length(); // HERE
那怎么会引发 NPE?
事实上,只有一种方法:只有当 foo
具有值 null
这种情况。然后我们尝试运行该 length()
方法 null
,然后...砰!
但是(我听到你说)如果在方法调用中抛出 NPE 怎么办 length()
?
好吧,如果发生这种情况,堆栈跟踪看起来会有所不同。第一行“at”会说异常是在类中的某行中引发的 java.lang.String
,而第 4 行将 Test.java
是第二行“at”。
那么,这是 null
从哪里来的呢?在这种情况下,这是显而易见的,我们需要做什么来修复它也是显而易见的。(为 分配一个非空值 foo
。)
好的,让我们尝试一个稍微复杂一点的例子。这需要一些 逻辑推理 .
public class Test {
private static String[] foo = new String[2];
private static int test(String[] bar, int pos) {
return bar[pos].length();
}
public static void main(String[] args) {
int length = test(foo, 1);
}
}
$ javac Test.java
$ java Test
Exception in thread "main" java.lang.NullPointerException
at Test.test(Test.java:6)
at Test.main(Test.java:10)
$
所以现在我们有了两个 \'at\' 行。第一个是针对此行的:
return args[pos].length();
第二个是针对这一行:
int length = test(foo, 1);
看看第一行,怎么会抛出 NPE?有两种方式:
-
如果
bar
值为 null
则将 bar[pos]
抛出 NPE。
-
的
bar[pos]
值为, null
则调用 length()
它将引发 NPE。
接下来,我们需要弄清楚哪种情况可以解释实际发生的情况。我们将从第一个开始探索:
哪里 bar
?它是方法调用的一个参数 test
,如果我们查看它是如何 test
被调用的,我们可以看到它来自 foo
静态变量。此外,我们可以清楚地看到我们将其初始化 foo
为非空值。这足以暂时驳回这个解释。(理论上,其他东西可能会 改变 foo
为 null
......但这里没有发生这种情况。)
那么我们的第二种情况呢?好吧,我们可以看到是 pos
, 1
所以这意味着 foo[1]
一定是 null
。这可能吗?
确实如此!这就是问题所在。当我们像这样初始化时:
private static String[] foo = new String[2];
我们分配一个 String[]
,其中包含两个 that are initialized to null
。之后,我们没有改变 foo
...的内容,所以 foo[1]
仍然是 null
.
那么在 Android 上怎么样?
在 Android 上,追踪 NPE 的直接原因要简单一些。异常消息通常会告诉您所使用的空引用的(编译时)类型 以及 抛出 NPE 时您尝试调用的方法。这简化了查明直接原因的过程。
但另一方面,Android 有一些常见的特定于平台的 NPE 原因。一种非常常见的情况是 getViewById
意外返回 null
。我的建议是搜索有关意外 null
返回值原因的问答。