什么是空指针异常 ( java.lang.NullPointerException
) 以及导致它们的原因?
可以使用哪些方法/工具来确定原因,以便阻止异常导致程序过早终止?
什么是空指针异常 ( java.lang.NullPointerException
) 以及导致它们的原因?
可以使用哪些方法/工具来确定原因,以便阻止异常导致程序过早终止?
当您声明一个引用变量(即一个对象)时,您实际上是在创建一个指向对象的指针。考虑以下代码,您在其中声明了一个原始类型的变量int
:
int x;
x = 10;
在这个例子中,变量x
是一个int
,Java 会0
为你初始化它。当您10
在第二行分配 的值时,您的值将10
写入 引用的内存位置x
。
但是,当您尝试声明引用类型时,会发生不同的事情。采取以下代码:
Integer num;
num = new Integer(10);
第一行声明了一个名为 的变量num
,但它实际上还没有包含原始值。相反,它包含一个指针(因为类型是Integer
引用类型)。由于您还没有说要指向什么,Java 将它设置为null
,这意味着“我没有指向任何东西”。
在第二行中,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();
}
在这种情况下,您不是在创建 object obj
,而是假设它是在doSomething()
调用方法之前创建的。请注意,可以像这样调用该方法:
doSomething(null);
在这种情况下,obj
is null
,并且该语句obj.myMethod()
将抛出NullPointerException
。
如果该方法打算像上面的方法那样对传入的对象执行某些操作,则抛出 是合适的,NullPointerException
因为这是程序员错误,程序员将需要该信息进行调试。
除了NullPointerException
作为方法逻辑的结果抛出 s 之外,您还可以通过在方法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 并且可以安全地取消引用。
或者,可能存在方法的目的不仅仅是对传入的对象进行操作的情况,因此可以接受空参数。在这种情况下,您需要检查空参数并采取不同的行为。您还应该在文档中解释这一点。例如,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
}
}
可以使用哪些方法/工具来确定原因,以便阻止异常导致程序过早终止?
带有查找错误的声纳可以检测 NPE。 sonar能否动态捕捉JVM引起的空指针异常
现在 Java 14 添加了一个新的语言特性来显示 NullPointerException 的根本原因。自 2006 年以来,此语言功能已成为 SAP 商业 JVM 的一部分。
在 Java 14 中,以下是示例 NullPointerException 异常消息:
在线程“main”中 java.lang.NullPointerException:无法调用“java.util.List.size()”,因为“list”为空
NullPointerException
s 是当您尝试使用指向内存中没有位置 (null) 的引用时发生的异常,就好像它正在引用一个对象一样。在空引用上调用方法或尝试访问空引用的字段将触发NullPointerException
. NullPointerException
这些是最常见的,但其他方法在javadoc 页面上列出。
可能我能想出的最快的示例代码来说明 aNullPointerException
将是:
public class Example {
public static void main(String[] args) {
Object obj = null;
obj.hashCode();
}
}
在里面的第一行main
,我明确地将Object
引用设置obj
为null
。这意味着我有一个引用,但它没有指向任何对象。之后,我尝试通过调用一个方法来将引用视为指向一个对象。这会导致 a NullPointerException
,因为在引用指向的位置没有要执行的代码。
(这是一个技术性问题,但我认为值得一提:指向 null 的引用与指向无效内存位置的 C 指针不同。空指针实际上并不指向任何地方,这与指向一个碰巧无效的位置。)
JavaDocs是一个很好的起点。他们涵盖了以下内容:
当应用程序在需要对象的情况下尝试使用 null 时引发。这些包括:
- 调用空对象的实例方法。
- 访问或修改空对象的字段。
- 将 null 的长度视为一个数组。
- 访问或修改 null 的槽,就像它是一个数组一样。
- 把 null 当作一个 Throwable 值来抛出。
应用程序应抛出此类的实例以指示空对象的其他非法使用。
根据 JLS,如果您尝试使用 null 引用synchronized
,也会引发此异常:
SynchronizedStatement: synchronized ( Expression ) Block
- 否则,如果 Expression 的值为 null,
NullPointerException
则抛出 a。
所以你有一个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();
}
}
识别空值
第一步是准确识别导致异常的值。为此,我们需要进行一些调试。学习阅读堆栈跟踪很重要。这将向您显示引发异常的位置:
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
是空的,调用它的length
方法会抛出异常。我们可以看到,当s.length()
从方法中删除时,程序停止抛出异常。
跟踪这些值的来源
接下来检查该值的来源。通过跟踪方法的调用者,我们看到在方法s
中传入了with ,并且为null。printString(name)
print()
this.name
跟踪应该设置这些值的位置
在哪里this.name
设置?在setName(String)
方法中。通过更多的调试,我们可以看到这个方法根本没有被调用。如果调用了该方法,请务必检查这些方法的调用顺序,并且 set 方法不会在print 方法之后调用。
这足以给我们一个解决方案:在调用printer.setName()
之前添加一个调用printer.print()
。
该变量可以有一个默认值(并且setName
可以防止它被设置为空):
private String name = "";
orprint
方法printString
可以检查 null,例如:
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 表示“无对象”的方式。
NullPointerException
每当您的程序尝试使用 a 时,就会在运行时抛出A null
,就好像它是一个真正的引用一样。例如,如果你这样写:
public class Test {
public static void main(String[] args) {
String foo = null;
int length = foo.length(); // HERE
}
}
标记为“HERE”的语句将尝试length()
在引用上运行该方法null
,这将抛出一个NullPointerException
.
您可以通过多种方式使用null
会产生NullPointerException
. 事实上,你可以用 anull
而不会导致 NPE 的唯一事情是:
==
or!=
运算符或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)
这告诉我们很多事情:
main
方法中Test
。如果您计算上面文件中的行数,第 4 行是我用“HERE”注释标记的行。
请注意,在更复杂的示例中,NPE 堆栈跟踪中会有很多行。但是您可以确定第二行(第一行“at”行)会告诉您 NPE 被抛出的位置1。
简而言之,堆栈跟踪将明确地告诉我们程序的哪个语句抛出了 NPE。
另请参阅:什么是堆栈跟踪,以及如何使用它来调试我的应用程序错误?
1 - 不完全正确。有些东西叫做嵌套异常......
这是困难的部分。简短的回答是将逻辑推理应用于堆栈跟踪、源代码和相关 API 文档提供的证据。
让我们先用简单的例子(上面)来说明。我们首先查看堆栈跟踪告诉我们发生 NPE 的位置:
int length = foo.length(); // HERE
那怎么能抛出NPE?
事实上,只有一种方法:只有当它foo
具有 value时才会发生null
。然后我们尝试运行该length()
方法,null
然后……砰!
但是(我听到你说)如果在length()
方法调用中抛出 NPE 怎么办?
好吧,如果发生这种情况,堆栈跟踪看起来会有所不同。第一个“at”行表示异常是在java.lang.String
类中的某行中引发的,第 4Test.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
is null
thenbar[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];
我们为 a 分配了两个初始化为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
指针是不指向任何地方的指针。当您取消引用指针p
时,您说“给我存储在“p”中的位置的数据。当p
是null
指针时,存储在的位置p
是nowhere
,您说的是“给我在'无处'位置的数据”。显然,它不能这样做,所以它抛出一个null pointer exception
.
一般来说,这是因为一些东西没有被正确初始化。
已经有很多解释来解释它是如何发生的以及如何修复它,但你也应该遵循最佳实践来完全避免NullPointerException
s。
另请参阅: 最佳实践的良好列表
我要补充一点,非常重要的是,善用final
修饰符。
在 Java 中适用时使用“final”修饰符
概括:
final
修饰符来强制执行良好的初始化。@NotNull
和@Nullable
if("knownObject".equals(unknownObject)
valueOf()
.toString()
StringUtils
方法StringUtils.isEmpty(null)
。在 Java 中,一切(不包括原始类型)都是类的形式。
如果你想使用任何对象,那么你有两个阶段:
例子:
Object object;
object = new Object();
数组概念也一样:
Item item[] = new Item[5];
item[0] = new Item();
如果你没有给出初始化部分,那么NullPointerException
就会出现。
空指针异常表明您正在使用对象但未对其进行初始化。
例如,下面是一个将在我们的代码中使用它的学生类。
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中,您声明的所有变量实际上都是对对象(或原语)的“引用”,而不是对象本身。
当您尝试执行一个对象方法时,引用会要求活动对象执行该方法。但是,如果引用引用的是 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
在保证的非空对象上使用。
数组内的所有元素都被初始化为其共同的初始值;对于任何类型的对象数组,这意味着所有元素都是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));
}