什么是空指针异常(java.lang.NullPointerException),是什么原因导致的?

可以使用哪些方法/工具来确定原因,从而阻止异常导致程序过早终止?


当前回答

问:什么原因导致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”的语句将尝试在空引用上运行length()方法,这将抛出NullPointerException。

有许多方法可以使用空值,从而导致NullPointerException异常。事实上,你可以在不引起NPE的情况下对null做的唯一事情是:

将其赋值给引用变量或从引用变量中读取, 将其赋值给数组元素或从数组元素中读取(前提是数组引用本身是非空的!) 将其作为参数传递或作为结果返回,或者 使用==或!=操作符或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)

这告诉我们一些事情:

“在测试。main表示我们在Test类的main方法中。 "Test.java:4"给出了类的源文件名,并告诉我们发生这种情况的语句位于文件的第4行。

如果你计算一下上面文件中的行数,第4行就是我用“HERE”注释标记的那行。

注意,在一个更复杂的示例中,NPE堆栈跟踪中将有很多行。但你可以肯定的是,第二行(第一个“at”行)会告诉你NPE扔在哪里n1。

简而言之,堆栈跟踪将明确地告诉我们程序的哪条语句抛出了NPE。

请参见:什么是堆栈跟踪,以及如何使用它来调试应用程序错误?

1 -不完全正确。有一种东西叫做嵌套异常……

问:如何在代码中追踪NPE异常的原因?

这是最难的部分。简单的回答是对堆栈跟踪、源代码和相关API文档提供的证据应用逻辑推理。

让我们先用上面的简单例子来说明。我们从堆栈跟踪告诉我们NPE发生的那行开始:

int length = foo.length(); // HERE

这怎么能引发NPE呢?

事实上,只有一种方法:只有在foo值为null时才会发生。然后,我们尝试在null上运行length()方法,并且…砰!

但是(我听到你说)如果NPE在length()方法调用中被抛出呢?

如果发生了这种情况,堆栈跟踪看起来就不一样了。第一个“at”行表示异常是在java.lang.String类中的某一行中抛出的,Test.java的第4行是第二个“at”行。

那么零是从哪里来的呢?在这种情况下,很明显,我们需要做什么来解决它。(为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的值为空,那么bar[pos]将抛出一个NPE。 如果bar[pos]的值为空,则对其调用length()将抛出一个NPE。

接下来,我们需要弄清楚这些场景中哪些解释了实际发生的事情。我们将从第一个开始:

bar是从哪里来的?它是test方法调用的参数,如果我们看看test是如何被调用的,我们可以看到它来自foo static变量。此外,我们可以清楚地看到,我们将foo初始化为一个非空值。这足以暂时否定这种解释。(理论上,其他一些东西可以将foo更改为null…但这并没有发生在这里。)

那么第二种情况呢?我们可以看到pos是1,这意味着foo[1]必须为空。这可能吗?

的确如此!这就是问题所在。当我们这样初始化时:

private static String[] foo = new String[2];

我们分配了一个String[],其中包含两个初始化为null的元素。在那之后,我们没有改变foo的内容…所以foo[1]仍然为空。

Android系统呢?

在Android上,追踪NPE的直接原因要简单一些。异常消息通常会告诉您正在使用的空引用的(编译时)类型以及抛出NPE时试图调用的方法。这简化了查明直接原因的过程。

但另一方面,Android有一些常见的平台特定原因导致npe。一个很常见的情况是getViewById意外返回null。我的建议是搜索有关意外空返回值的原因的Q&As。

其他回答

在Java中有两种主要类型的变量:

Primitives: variables that contain data. If you want to manipulate the data in a primitive variable you can manipulate that variable directly. By convention primitive types start with a lowercase letter. For example variables of type int or char are primitives. References: variables that contain the memory address of an Object i.e. variables that refer to an Object. If you want to manipulate the Object that a reference variable refers to you must dereference it. Dereferencing usually entails using . to access a method or field, or using [ to index an array. By convention reference types are usually denoted with a type that starts in uppercase. For example variables of type Object are references.

考虑下面的代码,其中你声明了一个基本类型为int的变量,并且没有初始化它:

int x;
int y = x + x;

这两行会使程序崩溃,因为没有为x指定值,而我们正试图使用x的值来指定y。所有原语在被操作之前都必须初始化为可用的值。

现在事情变得有趣了。引用变量可以设置为null,这意味着“我没有引用任何东西”。如果您以这种方式显式地设置引用变量,或者引用变量未初始化且编译器没有捕获它,则可以在引用变量中获得空值(Java将自动将变量设置为空)。

如果一个引用变量被您显式地设置为null,或者通过Java自动设置为null,并且您试图解除对它的引用,则会得到一个NullPointerException。

NullPointerException (NPE)通常发生在声明一个变量,但在尝试使用变量的内容之前没有创建对象并将其赋值给变量的情况下。所以你得到了一个并不存在的东西。

取以下代码:

Integer num;
num = new Integer(10);

第一行声明了一个名为num的变量,但它实际上还没有包含一个引用值。因为您还没有说指向什么,所以Java将其设置为null。

在第二行中,new关键字用于实例化(或创建)Integer类型的对象,并将引用变量num分配给该Integer对象。

如果你试图在创建对象之前解除对num的引用,你会得到一个NullPointerException。在大多数情况下,编译器会发现问题并告诉您“num可能没有初始化”,但有时您可能会编写不直接创建对象的代码。

例如,你可能有这样一个方法:

public void doSomething(SomeObject obj) {
   // Do something to obj, assumes obj is not null
   obj.myMethod();
}

在这种情况下,您不是在创建对象obj,而是假设它是在调用doSomething()方法之前创建的。注意,可以像这样调用该方法:

doSomething(null);

在这种情况下,obj为空,语句obj. mymethod()将抛出NullPointerException。

如果该方法打算像上面的方法那样对传入的对象做一些事情,那么抛出NullPointerException是合适的,因为这是一个程序员错误,程序员将需要该信息用于调试。

除了由方法逻辑引发的nullpointerexception异常,你还可以检查方法参数是否为空值,并通过在方法开头附近添加如下内容显式抛出npe:

// Throws an NPE with a custom error message if obj is null
Objects.requireNonNull(obj, "obj must not be null");

注意,在错误消息中清楚地说明哪个对象不能为空是很有帮助的。验证这一点的好处是:1)您可以返回自己更清晰的错误消息;2)对于方法的其余部分,您知道除非obj被重新赋值,否则它不是空的,可以安全地解除引用。

或者,在某些情况下,方法的目的不仅仅是对传入的对象进行操作,因此空参数可能是可以接受的。在这种情况下,您将需要检查空参数并采取不同的行为。您还应该在文档中对此进行解释。例如,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。 声纳能捕捉JVM动态引起的空指针异常吗

现在Java 14添加了一个新的语言特性来显示NullPointerException的根本原因。该语言特性自2006年以来一直是SAP商业JVM的一部分。

在Java 14中,下面是一个示例NullPointerException异常消息:

java.lang.NullPointerException:不能调用"java.util.List.size()",因为"list"是空的

导致NullPointerException发生的情况列表

以下是Java语言规范中直接提到的所有NullPointerException发生的情况:

Accessing (i.e. getting or setting) an instance field of a null reference. (static fields don't count!) Calling an instance method of a null reference. (static methods don't count!) throw null; Accessing elements of a null array. Synchronising on null - synchronized (someNullReference) { ... } Any integer/floating point operator can throw a NullPointerException if one of its operands is a boxed null reference An unboxing conversion throws a NullPointerException if the boxed value is null. Calling super on a null reference throws a NullPointerException. If you are confused, this is talking about qualified superclass constructor invocations:

class Outer {
    class Inner {}
}
class ChildOfInner extends Outer.Inner {
    ChildOfInner(Outer o) { 
        o.super(); // if o is null, NPE gets thrown
    }
}

Using a for (element : iterable) loop to loop through a null collection/array. switch (foo) { ... } (whether its an expression or statement) can throw a NullPointerException when foo is null. foo.new SomeInnerClass() throws a NullPointerException when foo is null. Method references of the form name1::name2 or primaryExpression::name throws a NullPointerException when evaluated when name1 or primaryExpression evaluates to null. a note from the JLS here says that, someInstance.someStaticMethod() doesn't throw an NPE, because someStaticMethod is static, but someInstance::someStaticMethod still throw an NPE!

*请注意,JLS可能也间接地说明了很多npe。

在Java中,您声明的所有变量实际上都是对象(或原语)的“引用”,而不是对象本身。

当您尝试执行一个对象方法时,引用会要求活动对象执行该方法。但是如果引用引用的是NULL (nothing, zero, void, nada),那么就没有办法执行方法。然后运行时通过抛出NullPointerException让你知道这一点。

你的引用是“指向”空,因此“空->指针”。

对象存在于VM内存空间中,访问它的唯一方法是使用该引用。举个例子:

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。

数组中的所有元素都被初始化为它们共同的初始值;对于任何类型的对象数组,这意味着所有元素都是空的。

在访问或取消引用数组中的元素之前,必须初始化它们。

String[] phrases = new String[] {"The bird", "A bird", "My bird", "Bird"};
String keyPhrase = "Bird";
for(String phrase : phrases) {
    System.out.println(phrase.equals(keyPhrase));
}

空指针异常表示您正在使用一个对象而没有初始化它。

例如,下面是一个学生类,将在我们的代码中使用它。

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");
        }
    }
}

已经有很多解释来解释它是如何发生的以及如何修复它,但是您还应该遵循最佳实践来避免nullpointerexception。

参见: 一个很好的最佳实践列表

我还要补充一点,很重要的一点是,充分利用最后一个修饰语。 在Java中使用“final”修饰符

简介:

Use the final modifier to enforce good initialization. Avoid returning null in methods, for example returning empty collections when applicable. Use annotations @NotNull and @Nullable Fail fast and use asserts to avoid propagation of null objects through the whole application when they shouldn't be null. Use equals with a known object first: if("knownObject".equals(unknownObject) Prefer valueOf() over toString(). Use null safe StringUtils methods StringUtils.isEmpty(null). Use Java 8 Optional as return value in methods, Optional class provide a solution for representing optional values instead of null references.