【发布时间】:2025-12-03 15:40:01
【问题描述】:
什么是空指针异常 (java.lang.NullPointerException) 以及导致它们的原因?
可以使用哪些方法/工具来确定原因,从而阻止异常导致程序过早终止?
【问题讨论】:
什么是空指针异常 (java.lang.NullPointerException) 以及导致它们的原因?
可以使用哪些方法/工具来确定原因,从而阻止异常导致程序过早终止?
【问题讨论】:
当你声明一个引用变量(即一个对象)时,你实际上是在创建一个指向一个对象的指针。考虑下面的代码,你声明了一个原始类型的变量int:
int x;
x = 10;
在本例中,变量x 是int,Java 将为您将其初始化为0。当您在第二行分配10 的值时,您的10 值将写入x 引用的内存位置。
但是,当您尝试声明引用type 时,会发生不同的情况。取以下代码:
Integer num;
num = new Integer(10);
第一行声明了一个名为num 的变量,但它实际上还没有包含原始值。相反,它包含一个指针(因为类型是Integer,它是一个引用类型)。由于您还没有说要指向什么,Java 将其设置为 null,这意味着“我正在指向 nothing”。
在第二行中,new 关键字用于实例化(或创建)Integer 类型的对象,并将指针变量num 分配给该Integer 对象。
NullPointerException (NPE) 发生在您声明一个变量但未创建对象并将其分配给该变量之前尝试使用该变量的内容(称为取消引用)。所以你指向的东西实际上并不存在。
取消引用通常发生在使用. 访问方法或字段,或使用[ 索引数组时。
如果您尝试在创建对象之前取消引用num,您将获得NullPointerException。在最琐碎的情况下,编译器会发现问题并让您知道“num may not have been initialized”,但有时您可能会编写不直接创建对象的代码。
例如,你可能有一个方法如下:
public void doSomething(SomeObject obj) {
// Do something to obj, assumes obj is not null
obj.myMethod();
}
在这种情况下,您不是在创建对象obj,而是假设它是在调用doSomething() 方法之前创建的。注意,可以这样调用方法:
doSomething(null);
在这种情况下,obj 是 null,而语句 obj.myMethod() 将抛出 NullPointerException。
如果该方法打算像上述方法那样对传入的对象执行某些操作,则抛出 NullPointerException 是合适的,因为这是程序员错误,程序员将需要该信息进行调试。
除了作为方法逻辑的结果抛出NullPointerExceptions 之外,您还可以检查null 值的方法参数并通过在方法开头附近添加类似以下内容来显式抛出NPE:
// Throws an NPE with a custom error message if obj is null
Objects.requireNonNull(obj, "obj must not be null");
请注意,在错误消息中明确说明哪个 对象不能是null 会很有帮助。验证这一点的好处是 1) 您可以返回自己的更清晰的错误消息,以及 2) 对于您知道的其余方法,除非重新分配 obj,否则它不为空并且可以安全地取消引用。
另外,在某些情况下,方法的目的不仅仅是对传入的对象进行操作,因此可以接受 null 参数。在这种情况下,您需要检查 null 参数 并采取不同的行为。您还应该在文档中解释这一点。例如,doSomething() 可以写成:
/**
* @param obj An optional foo for ____. May be null, in which case
* the result will be ____.
*/
public void doSomething(SomeObject obj) {
if(obj == null) {
// Do something
} else {
// Do something else
}
}
最后,How to pinpoint the exception & cause using Stack Trace
可以使用哪些方法/工具来确定原因,以便您停止 导致程序提前终止的异常?
带有查找错误的声纳可以检测 NPE。 Can sonar catch null pointer exceptions caused by JVM Dynamically
现在 Java 14 添加了一个新的语言特性来显示 NullPointerException 的根本原因。自 2006 年以来,此语言功能已成为 SAP 商业 JVM 的一部分。
在 Java 14 中,以下是 NullPointerException 异常消息示例:
在线程“main”中 java.lang.NullPointerException:无法调用“java.util.List.size()”,因为“list”为空
【讨论】:
Integer,int a=b 可以抛出 NPE。在某些情况下,这会使调试感到困惑。
NullPointerException 问题的方法是使用@Nullable 和@NotNull 注释。以下answer 对此有更多信息。尽管此答案专门针对 IntelliJ IDE,但它也适用于其他工具,例如来自 teh cmets 的 apparanet。 (顺便说一句,我不允许直接编辑这个答案,也许作者可以添加它?)
NullPointerExceptions 是当您尝试使用指向内存中没有位置 (null) 的引用时发生的异常,就好像它正在引用一个对象一样。在空引用上调用方法或尝试访问空引用的字段将触发NullPointerException。这些是最常见的,但NullPointerException javadoc 页面上列出了其他方式。
我能想到的用于说明NullPointerException 的最快示例代码可能是:
public class Example {
public static void main(String[] args) {
Object obj = null;
obj.hashCode();
}
}
在main 内的第一行,我明确设置Object 引用obj 等于null。这意味着我有一个引用,但它没有指向任何对象。之后,我尝试通过调用一个方法来将引用视为指向一个对象。这会导致NullPointerException,因为在引用指向的位置没有要执行的代码。
(这是一个技术问题,但我认为值得一提:指向 null 的引用与指向无效内存位置的 C 指针不同。空指针实际上并不指向 任何地方,这与指向恰好无效的位置略有不同。)
【讨论】:
null 以外的其他值。使用局部变量,编译器会捕捉到这个错误,但在这种情况下它不会。也许这会对您的答案有所帮助?
JavaDocs 是一个很好的起点。他们涵盖了这一点:
在应用程序尝试使用 null 的情况下抛出 对象是必需的。其中包括:
- 调用空对象的实例方法。
- 访问或修改空对象的字段。
- 将 null 的长度视为数组。
- 访问或修改 null 的槽,就像它是一个数组一样。
- 将 null 视为 Throwable 值。
应用程序应抛出此类的实例以指示其他 非法使用空对象。
同样的情况是,如果您尝试对synchronized 使用空引用,也会抛出此异常per the JLS:
SynchronizedStatement: synchronized ( Expression ) Block
- 否则,如果 Expression 的值为 null,则抛出
NullPointerException。
所以你有一个NullPointerException。你如何解决它?让我们举一个简单的例子,它抛出一个NullPointerException:
public class Printer {
private String name;
public void setName(String name) {
this.name = name;
}
public void print() {
printString(name);
}
private void printString(String s) {
System.out.println(s + " (" + s.length() + ")");
}
public static void main(String[] args) {
Printer printer = new Printer();
printer.print();
}
}
识别空值
第一步是准确识别导致异常的值。为此,我们需要进行一些调试。学习阅读 stacktrace 很重要。这将向您显示引发异常的位置:
Exception in thread "main" java.lang.NullPointerException
at Printer.printString(Printer.java:13)
at Printer.print(Printer.java:9)
at Printer.main(Printer.java:19)
在这里,我们看到在第 13 行抛出了异常(在 printString 方法中)。查看该行并通过以下方式检查哪些值为空
添加日志语句或使用调试器。我们发现s 为null,在其上调用length 方法会抛出异常。我们可以看到,当从方法中删除s.length() 时,程序停止抛出异常。
追踪这些值的来源
接下来检查这个值的来源。通过跟踪方法的调用者,我们看到s在print()方法中是用printString(name)传入的,this.name为null。
跟踪这些值的设置位置
this.name 设置在哪里?在setName(String) 方法中。通过更多的调试,我们可以看到这个方法根本没有被调用。如果调用了该方法,请务必检查调用这些方法的顺序,并且 set 方法没有在 print 方法之后调用。
这足以给我们一个解决方案:在调用 printer.print() 之前添加对 printer.setName() 的调用。
变量可以有一个默认值(并且setName可以防止它被设置为null):
private String name = "";
print 或 printString 方法都可以检查是否为空,例如:
printString((name == null) ? "" : name);
或者你可以设计类,使name总是有一个非空值:
public class Printer {
private final String name;
public Printer(String name) {
this.name = Objects.requireNonNull(name);
}
public void print() {
printString(name);
}
private void printString(String s) {
System.out.println(s + " (" + s.length() + ")");
}
public static void main(String[] args) {
Printer printer = new Printer("123");
printer.print();
}
}
另请参阅:
如果您尝试调试问题但仍然没有解决方案,您可以发布问题以获得更多帮助,但请确保包含您迄今为止尝试过的内容。至少,在问题中包含堆栈跟踪,并在代码中标记重要的行号。另外,请先尝试简化代码(请参阅SSCCE)。
【讨论】:
NullPointerException (NPE)?如您所知,Java 类型分为原始类型(boolean、int 等)和引用类型。 Java 中的引用类型允许您使用特殊值 null,这是 Java 表示“无对象”的方式。
每当您的程序尝试使用null 时,就会在运行时抛出NullPointerException,就好像它是一个真正的引用一样。例如,如果你这样写:
public class Test {
public static void main(String[] args) {
String foo = null;
int length = foo.length(); // HERE
}
}
标记为“HERE”的语句将尝试在 null 引用上运行 length() 方法,这将引发 NullPointerException。
您可以通过多种方式使用null 值,从而生成NullPointerException。事实上,您可以对null 做的唯一事情是:
== 或!= 运算符或instanceof 对其进行测试。假设我编译运行上面的程序:
$ javac Test.java
$ java Test
Exception in thread "main" java.lang.NullPointerException
at Test.main(Test.java:4)
$
第一观察:编译成功!程序中的问题不是编译错误。这是一个运行时错误。 (某些 IDE 可能会警告您的程序总是会抛出异常……但标准的 javac 编译器不会。)
第二个观察:当我运行程序时,它会输出两行“gobbledy-gook”。 错了!! 这不是狼吞虎咽。它是一个堆栈跟踪......它提供了重要信息,如果您花时间仔细阅读它,它将帮助您追踪代码中的错误。
那么让我们看看它是怎么说的:
Exception in thread "main" java.lang.NullPointerException
堆栈跟踪的第一行告诉你一些事情:
java.lang.NullPointerException。NullPointerException 在这方面是不寻常的,因为它很少有错误消息。第二行是诊断 NPE 中最重要的一行。
at Test.main(Test.java:4)
这告诉我们一些事情:
Test 类的 main 方法中。如果您计算上面文件中的行数,第 4 行就是我用“HERE”注释标记的行。
请注意,在更复杂的示例中,NPE 堆栈跟踪中会有很多行。但是您可以确定第二行(第一行“at”行)会告诉您 NPE 被抛出的位置1。
简而言之,堆栈跟踪将明确地告诉我们程序的哪个语句抛出了 NPE。
另请参阅:What is a stack trace, and how can I use it to debug my application errors?
1 - 不完全正确。有些东西叫做嵌套异常......
这是最难的部分。简短的回答是对堆栈跟踪、源代码和相关 API 文档提供的证据进行逻辑推理。
让我们先用简单的例子(上面)来说明。我们首先查看堆栈跟踪告诉我们发生 NPE 的位置:
int length = foo.length(); // HERE
这怎么会引发 NPE?
事实上,只有一种方法:只有当foo 的值为null 时才会发生。然后我们尝试在null 上运行length() 方法,然后......砰!
但是(我听到你说)如果 NPE 被抛出到 length() 方法调用中怎么办?
好吧,如果发生这种情况,堆栈跟踪看起来会有所不同。第一个“at”行表示异常是在 java.lang.String 类的某行中引发的,Test.java 的第 4 行将是第二个“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[],其中有两个元素初始化为null。在那之后,我们并没有改变foo的内容……所以foo[1]仍然是null。
在 Android 上,追踪 NPE 的直接原因要简单一些。异常消息通常会告诉您正在使用的空引用的(编译时)类型和您在抛出 NPE 时尝试调用的方法。这简化了查明直接原因的过程。
但另一方面,Android 对 NPE 有一些常见的特定于平台的原因。一个很常见的情况是getViewById 意外返回null。我的建议是搜索关于意外null 返回值原因的问答。
【讨论】:
这就像你试图访问一个对象 null。考虑下面的例子:
TypeA objA;
此时您刚刚声明此对象,但尚未初始化或实例化。并且每当您尝试访问其中的任何属性或方法时,它都会抛出NullPointerException,这是有道理的。
请参见下面的示例:
String a = null;
System.out.println(a.toString()); // NullPointerException will be thrown
【讨论】:
当应用程序在需要对象的情况下尝试使用 null 时,会引发空指针异常。其中包括:
null 对象的实例方法。null 对象的字段。null 的长度视为一个数组。null 的槽。null 视为 Throwable 值。 应用程序应抛出此类的实例以指示 null 对象的其他非法用途。
参考:http://docs.oracle.com/javase/8/docs/api/java/lang/NullPointerException.html
【讨论】:
null 作为synchronized 块的目标,2) 使用null 作为switch 的目标,然后取消装箱@ 987654332@.
null 指针是指向任何地方的指针。当您取消引用指针p 时,您说“给我存储在“p”中的位置的数据。当p 是null 指针时,存储在p 中的位置是nowhere,您重新说“给我'无处'位置的数据”。显然,它不能这样做,所以它会抛出一个null pointer exception。
一般来说,这是因为一些东西没有被正确初始化。
【讨论】:
已经有很多解释来解释它是如何发生的以及如何解决它,但你也应该遵循最佳实践来完全避免NullPointerExceptions。
另请参阅: A good list of best practices
我要补充一点,非常重要的是,充分利用 final 修饰符。
Using the "final" modifier whenever applicable in Java
总结:
final 修饰符强制执行良好的初始化。@NotNull 和@Nullable
if("knownObject".equals(unknownObject)
valueOf() 而不是toString()。StringUtils方法StringUtils.isEmpty(null)。【讨论】:
@Nullable)并警告潜在错误。也可以基于现有的代码结构推断和生成这样的注释(例如 IntelliJ 可以做到)。
if (obj==null)。如果它是空的,那么你也应该编写代码来处理它。
空指针异常表明您正在使用对象但未对其进行初始化。
例如,下面是一个将在我们的代码中使用它的学生类。
public class Student {
private int id;
public int getId() {
return this.id;
}
public setId(int newId) {
this.id = newId;
}
}
下面的代码给你一个空指针异常。
public class School {
Student student;
public School() {
try {
student.getId();
}
catch(Exception e) {
System.out.println("Null pointer exception");
}
}
}
因为您使用的是student,但是您忘记像在
正确的代码如下所示:
public class School {
Student student;
public School() {
try {
student = new Student();
student.setId(12);
student.getId();
}
catch(Exception e) {
System.out.println("Null pointer exception");
}
}
}
【讨论】:
在 Java 中,一切(不包括原始类型)都是类的形式。
如果你想使用任何对象,那么你有两个阶段:
例子:
Object object;
object = new Object();
数组概念也一样:
Item item[] = new Item[5];
item[0] = new Item();
如果您不提供初始化部分,则会出现 NullPointerException。
【讨论】:
在Java 中,您声明的所有变量实际上都是对对象(或原语)的“引用”,而不是对象本身。
当您尝试执行一个对象方法时,引用会要求活动对象执行该方法。但是,如果引用引用的是 NULL(无、零、无效、nada),则该方法无法执行。然后运行时通过抛出 NullPointerException 让您知道这一点。
您的引用是“指向”null,因此是“Null -> Pointer”。
对象位于 VM 内存空间中,访问它的唯一方法是使用 this 引用。举个例子:
public class Some {
private int id;
public int getId(){
return this.id;
}
public setId( int newId ) {
this.id = newId;
}
}
在你的代码中的另一个地方:
Some reference = new Some(); // Point to a new object of type Some()
Some otherReference = null; // Initiallly this points to NULL
reference.setId( 1 ); // Execute setId method, now private var id is 1
System.out.println( reference.getId() ); // Prints 1 to the console
otherReference = reference // Now they both point to the only object.
reference = null; // "reference" now point to null.
// But "otherReference" still point to the "real" object so this print 1 too...
System.out.println( otherReference.getId() );
// Guess what will happen
System.out.println( reference.getId() ); // :S Throws NullPointerException because "reference" is pointing to NULL remember...
这是一件很重要的事情——当不再有对某个对象的引用时(在上面的示例中,reference 和 otherReference 都指向 null),那么该对象是“不可访问的”。我们无法使用它,因此该对象已准备好进行垃圾回收,并且在某个时候,VM 将释放该对象使用的内存并分配另一个。
【讨论】:
NullPointerException 的另一种情况发生在声明对象数组时,然后立即尝试取消引用其中的元素。
String[] phrases = new String[10];
String keyPhrase = "Bird";
for(String phrase : phrases) {
System.out.println(phrase.equals(keyPhrase));
}
如果颠倒比较顺序,可以避免这种特殊的 NPE;即,在保证的非空对象上使用.equals。
数组are initialized to their common initial value内的所有元素;对于任何类型的对象数组,这意味着所有元素都是null。
您必须在访问或取消引用它们之前初始化数组中的元素。
String[] phrases = new String[] {"The bird", "A bird", "My bird", "Bird"};
String keyPhrase = "Bird";
for(String phrase : phrases) {
System.out.println(phrase.equals(keyPhrase));
}
【讨论】:
Optional 之前的模式是返回 null。关键字很好。知道如何防范它是至关重要的。这提供了一种常见的情况以及缓解它的方法。