我已经注意到Oracle JDK中的许多Java 8方法使用Objects.requireNonNull(),如果给定的对象(参数)为空,它会在内部抛出NullPointerException。

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

但是如果空对象被解引用,无论如何都会抛出NullPointerException。那么,为什么要做这个额外的空检查并抛出 NullPointerException吗?

一个明显的答案(或好处)是它使代码更具可读性,我同意这一点。我很想知道使用的其他原因 方法开头的Objects.requireNonNull()。


当前回答

Using requireNonNull() as first statements in a method allow to identify right now/fast the cause of the exception. The stacktrace indicates clearly that the exception was thrown as soon as the entry of the method because the caller didn't respect the requirements/contract. Passing a null object to another method may indeed provoke an exception at a time but the cause of the problem may be more complicated to understand as the exception will be thrown in a specific invocation on the null object that may be much further.


下面是一个具体而真实的例子,它展示了为什么我们必须在一般情况下支持快速失败,特别是使用Object.requireNonNull()或任何方式对设计为非空的参数执行无空检查。

假设有一个Dictionary类,它组成一个LookupService和一个String列表,表示包含在。这些字段被设计为非空,其中一个字段被传递给Dictionary构造函数。

现在假设Dictionary有一个“糟糕的”实现,在方法条目中没有空检查(这里是构造函数):

public class Dictionary {

    private final List<String> words;
    private final LookupService lookupService;

    public Dictionary(List<String> words) {
        this.words = this.words;
        this.lookupService = new LookupService(words);
    }

    public boolean isFirstElement(String userData) {
        return lookupService.isFirstElement(userData);
    }        
}


public class LookupService {

    List<String> words;

    public LookupService(List<String> words) {
        this.words = words;
    }

    public boolean isFirstElement(String userData) {
        return words.get(0).contains(userData);
    }
}

现在,让我们调用Dictionary构造函数,对words形参使用空引用:

Dictionary dictionary = new Dictionary(null); 

// exception thrown lately : only in the next statement
boolean isFirstElement = dictionary.isFirstElement("anyThing");

JVM在下面的语句中抛出NPE:

return words.get(0).contains(userData); 
Exception in thread "main" java.lang.NullPointerException
    at LookupService.isFirstElement(LookupService.java:5)
    at Dictionary.isFirstElement(Dictionary.java:15)
    at Dictionary.main(Dictionary.java:22)

The exception is triggered in the LookupService class while the origin of it is well earlier (the Dictionary constructor). It makes the overall issue analysis much less obvious. Is words null? Is words.get(0) null ? Both ? Why the one, the other or maybe both are null ? Is it a coding error in Dictionary (constructor? invoked method?) ? Is it a coding error in LookupService ? (constructor? invoked method?) ? Finally, we will have to inspect more code to find the error origin and in a more complex class maybe even use a debugger to understand more easily what it happened. But why a simple thing (a lack of null check) become a complex issue ? Because we allowed the initial bug/lack identifiable on a specific component leak on lower components. Imagine that LookupService was not a local service but a remote service or a third party library with few debugging information or imagine that you didn't have 2 layers but 4 or 5 layers of object invocations before that the null be detected ? The problem would be still more complex to analyse.

所以帮忙的方法是:

public Dictionary(List<String> words) {
    this.words = Objects.requireNonNull(words);
    this.lookupService = new LookupService(words);
}

这样就不会让人头疼了:只要接收到这个,我们就会抛出异常:

// exception thrown early : in the constructor 
Dictionary dictionary = new Dictionary(null);

// we never arrive here
boolean isFirstElement = dictionary.isFirstElement("anyThing");
Exception in thread "main" java.lang.NullPointerException
    at java.util.Objects.requireNonNull(Objects.java:203)
    at com.Dictionary.(Dictionary.java:15)
    at com.Dictionary.main(Dictionary.java:24)

注意,这里我用构造函数说明了这个问题,但方法调用可以有相同的非空检查约束。

其他回答

作为旁注,在object# requireNotNull之前,在java-9之前的一些jre类中实现的这个失败速度略有不同。假设如下情况:

 Consumer<String> consumer = System.out::println;

在java-8中,编译为(仅相关部分)

getstatic Field java/lang/System.out
invokevirtual java/lang/Object.getClass

基本上是一个操作:yourReference。getClass -如果你的引用为空,它将失败。

在jdk-9中发生了变化,相同的代码编译为

getstatic Field java/lang/System.out
invokestatic java/util/Objects.requireNonNull

或者基本上是对象。requireNotNull(一份)

快速失败

代码应该会尽快崩溃。它不应该做一半的工作,解引用null,然后才崩溃,只做了一半的工作,导致系统处于无效状态。

这通常被称为“早期失败”或“快速失败”。

我认为它应该用于复制构造函数和其他一些情况,如DI,其输入参数是一个对象,你应该检查参数是否为空。 在这种情况下,您可以方便地使用此静态方法。

Using requireNonNull() as first statements in a method allow to identify right now/fast the cause of the exception. The stacktrace indicates clearly that the exception was thrown as soon as the entry of the method because the caller didn't respect the requirements/contract. Passing a null object to another method may indeed provoke an exception at a time but the cause of the problem may be more complicated to understand as the exception will be thrown in a specific invocation on the null object that may be much further.


下面是一个具体而真实的例子,它展示了为什么我们必须在一般情况下支持快速失败,特别是使用Object.requireNonNull()或任何方式对设计为非空的参数执行无空检查。

假设有一个Dictionary类,它组成一个LookupService和一个String列表,表示包含在。这些字段被设计为非空,其中一个字段被传递给Dictionary构造函数。

现在假设Dictionary有一个“糟糕的”实现,在方法条目中没有空检查(这里是构造函数):

public class Dictionary {

    private final List<String> words;
    private final LookupService lookupService;

    public Dictionary(List<String> words) {
        this.words = this.words;
        this.lookupService = new LookupService(words);
    }

    public boolean isFirstElement(String userData) {
        return lookupService.isFirstElement(userData);
    }        
}


public class LookupService {

    List<String> words;

    public LookupService(List<String> words) {
        this.words = words;
    }

    public boolean isFirstElement(String userData) {
        return words.get(0).contains(userData);
    }
}

现在,让我们调用Dictionary构造函数,对words形参使用空引用:

Dictionary dictionary = new Dictionary(null); 

// exception thrown lately : only in the next statement
boolean isFirstElement = dictionary.isFirstElement("anyThing");

JVM在下面的语句中抛出NPE:

return words.get(0).contains(userData); 
Exception in thread "main" java.lang.NullPointerException
    at LookupService.isFirstElement(LookupService.java:5)
    at Dictionary.isFirstElement(Dictionary.java:15)
    at Dictionary.main(Dictionary.java:22)

The exception is triggered in the LookupService class while the origin of it is well earlier (the Dictionary constructor). It makes the overall issue analysis much less obvious. Is words null? Is words.get(0) null ? Both ? Why the one, the other or maybe both are null ? Is it a coding error in Dictionary (constructor? invoked method?) ? Is it a coding error in LookupService ? (constructor? invoked method?) ? Finally, we will have to inspect more code to find the error origin and in a more complex class maybe even use a debugger to understand more easily what it happened. But why a simple thing (a lack of null check) become a complex issue ? Because we allowed the initial bug/lack identifiable on a specific component leak on lower components. Imagine that LookupService was not a local service but a remote service or a third party library with few debugging information or imagine that you didn't have 2 layers but 4 or 5 layers of object invocations before that the null be detected ? The problem would be still more complex to analyse.

所以帮忙的方法是:

public Dictionary(List<String> words) {
    this.words = Objects.requireNonNull(words);
    this.lookupService = new LookupService(words);
}

这样就不会让人头疼了:只要接收到这个,我们就会抛出异常:

// exception thrown early : in the constructor 
Dictionary dictionary = new Dictionary(null);

// we never arrive here
boolean isFirstElement = dictionary.isFirstElement("anyThing");
Exception in thread "main" java.lang.NullPointerException
    at java.util.Objects.requireNonNull(Objects.java:203)
    at com.Dictionary.(Dictionary.java:15)
    at com.Dictionary.main(Dictionary.java:24)

注意,这里我用构造函数说明了这个问题,但方法调用可以有相同的非空检查约束。

除了所有正确答案之外:

我们在反应流中使用它。通常产生的nullpointerexception根据它们在流中的出现情况被包装成其他异常。因此,我们以后可以很容易地决定如何处理错误。

举个例子:假设你有

<T> T parseAndValidate(String payload) throws ParsingException { ... };
<T> T save(T t) throws DBAccessException { ... };

其中parseAndValidate从ParsingException中的requireNonNull中wrapps NullPointerException。

现在你可以决定,例如什么时候重试或不重试:

...
.map(this::parseAndValidate)
.map(this::save)
.retry(Retry.<T>allBut(ParsingException.class))

如果不进行检查,将在save方法中发生NullPointerException,这将导致无休止的重试。甚至值得,想象一个长寿的订阅,补充道

.onErrorContinue(
    throwable -> throwable.getClass().equals(ParsingException.class),
    parsingExceptionConsumer()
)

现在retryextrestexception将终止您的订阅。