为什么应该使用Objects.requireNonNull()?

441

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

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

但是,如果引用了一个空对象,无论如何都会抛出NullPointerException。那么,为什么还要进行额外的空检查并抛出NullPointerException呢?

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


44
https://en.wikipedia.org/wiki/Fail-fast - luk2302
4
这种参数检查方法可以让你在编写单元测试时作弊。Spring也有类似的工具(请参见https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/util/Assert.html)。如果你有一个“if”语句,并且想要高覆盖率的单元测试,你必须涵盖两个分支:当条件满足时和条件不满足时。如果你使用`Objects.requireNonNull`,你的代码没有分支,所以单元测试的一次通过就可以获得100%的覆盖率 :-) - xorcus
@javing,你打算如何确保没有人会传递空值而不出现错误呢? - SamB
我在这里是因为IntelliJ建议在任何地方都使用requireNonNull。如果下面的示例有意义,我必须承认IntelliJ建议的修复并不总是有效的。 - Christian Vincenzo Traina
显示剩余2条评论
12个回答

397

因为你可以通过这样做使事情更加明确,例如:

public class Foo {
  private final Bar bar;

  public Foo(Bar bar) {
    Objects.requireNonNull(bar, "bar must not be null");
    this.bar = bar;
  }

或更短:

  this.bar = Objects.requireNonNull(bar, "bar must not be null");

现在你已经知道:

  • 使用new()方法成功创建一个Foo对象的时间
  • 这个对象的bar字段保证不为空。

相比之下:今天你创建了一个Foo对象,明天你调用一个使用该字段并抛出异常的方法。很可能,你明天不知道为什么这个引用在昨天传递给构造函数时为空!

换句话说:通过显式地使用这种方法来检查传入引用,可以控制异常抛出的时间点。而大多数时候,您希望尽快失败

主要优点包括:

  • 如上所述,可控性
  • 更易于调试-因为您在对象创建的上下文中抛出异常。在某个时间点上,您有一定的机会让日志/跟踪告诉您出了什么问题!
  • 正如上面所示:这个想法的真正威力是与final字段结合使用。因为现在,类中的任何其他代码都可以安全地假定bar不为null-因此您不需要在其他地方进行任何if(bar == null)检查!

50
你可以通过写入this.bar = Objects.requireNonNull(bar, "bar must not be null");来使你的代码更加简洁。 - Kirill Rakhman
6
@KirillRakhman教GhostCat了一些我不知道的酷东西--> 赢得GhostCat点赞抽奖的一张彩票。 - GhostCat
2
我认为这里的评论#1是Objects.requireNonNull(bar,“bar不能为空”);的实际好处。谢谢你。 - Kawu
6
构造函数中使用this.bar = Objects.requireNonNull(bar, "bar must not be null");是可以的,但如果在同一方法中设置两个或更多变量,则在其他方法中使用此语句可能会带来潜在的危险。例如:https://talkwards.com/2018/11/03/one-thing-to-avoid-when-using-objects-requirenonnull-in-java-8/ - Erk
4
由于Objects.requireNonNull是一个静态方法,可以使用静态导入使代码变得更短、更易读:this.bar = requireNonNull(...) - maxime.bochon
显示剩余7条评论

177

Fail-fast

代码应该尽早崩溃。它不应该完成一半的工作,解引用空指针,然后才崩溃,留下了一些未完成的工作,导致系统处于无效状态。

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


我相信这是对问题的准确回答。 - sankar

53
如果对空对象进行解除引用操作,则仍会抛出NullPointerException异常。那么,为什么还要进行额外的空检查并抛出NullPointerException异常呢?
这意味着您可以“立即”和“可靠地”检测到问题。
考虑:
- 可能直到方法后面才使用引用,此时您的代码已执行一些副作用 - 在此方法中可能根本不引用该引用
- 它可能传递给完全不同的代码(即原因和错误在代码空间上相隔很远) - 它可能被延迟使用(即原因和错误在时间上相隔很远)
- 它可能在允许为空引用的某个地方使用,但会产生意外效果。
.NET通过将NullReferenceException(“您对null值进行了解除引用操作”)与ArgumentNullException(“您不应该将null作为参数传递 - 而且是针对此参数”。我希望Java也能做到这一点,但即使只有NullPointerException,如果在可以检测到错误的最早点处抛出错误,修复代码仍然更容易。

28

在方法中使用requireNonNull()作为第一条语句可以立即/快速地确定异常的原因。堆栈跟踪明确指出,异常是因为调用者未遵守要求/合同而在方法入口处抛出的。将null对象传递给另一个方法可能会在某个时刻引发异常,但问题的原因可能更加复杂,因为异常将在可能远得多的null对象上的特定调用中抛出。


这里有一个具体的实例,展示了为什么我们必须通常采用快速失败,尤其是使用Object.requireNonNull()或任何检查旨在不为null的参数的方法。

假设有一个Dictionary类,组成了一个LookupService和一个包含单词的StringList。这些字段旨在不为null,并且其中之一在Dictionary构造函数中被传递。

现在假设构造函数没有null检查的“坏”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);
    }
}

现在,让我们使用null引用作为words参数来调用Dictionary构造函数:

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); 
LookupService 类中触发了异常,但其根源要早得多(即 Dictionary 构造函数)。这使得整个问题分析变得不太明显。
wordsnull吗? words.get(0)null吗?两者都是吗?
为什么一个或另一个,或者可能两者都是null?
这是Dictionary(构造函数?调用的方法?)中的编码错误吗?还是LookupService 中的编码错误?(构造函数?调用的方法?)
最后,我们将不得不检查更多代码以找到错误的来源,在更复杂的类中甚至可能需要使用调试器来更轻松地理解发生了什么。
但是为什么一个简单的事情(缺少空指针检查)会变成一个复杂的问题?
因为我们允许初始 bug/缺陷在特定组件上泄漏到较低级别的组件。
想象一下,如果LookupService 不是本地服务,而是远程服务或第三方库,具有很少的调试信息,或者想象一下在null被检测之前有 2 层,而不是 4 或 5 层的对象调用?问题仍然更复杂。 
因此,有利的方法是:

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");
在主线程中发生异常:"main" java.lang.NullPointerException
    位于java.util.Objects.requireNonNull(Objects.java:203)
    在com.Dictionary.(Dictionary.java:15)
    在com.Dictionary.main(Dictionary.java:24)

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


2
哇!讲解得非常好。 - Jonathas Nascimento
2
我同意@JonathasNascimento的观点,非常好的解释和使用示例。 - MarkyMarksFunkyBunch

17

NPE(空指针异常)是在稍后访问对象的成员时,访问一个null值时抛出的异常。 Objects.requireNonNull()从访问该值的位置回溯到将其初始化为null的位置,因此可以专注于NPE的真实来源。

最简单的情况:

// Global String
String nullString = null;

调用空值:

public void useString() {
    nullString.length();  // The source of exception, without using Objects.requireNonNull()
}
现在,如果我们在初始化期间使用 Objects.requireNonNull()
// Global String
String nullString = Objects.requireNonNull(null); // The source of exception which is indeed the actual source of NPE

2
我来重新表述一下:它立即指出了空值发生的位置和时间,而不是第一次访问该值时(使用NPE)。 - Violet Giraffe

2
作为一个附注,在Java 9之前,一些JRE类本身内部实现的快速失败与Object#requireNotNull稍有不同。假设以下情况:
 Consumer<String> consumer = System.out::println;

在 Java 8 中,这段代码编译后如下所示(仅显示相关部分)

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

基本上,操作如下:yourReference.getClass - 如果yourReferencenull则会失败。

在jdk-9中,事情发生了变化,相同的代码编译结果如下:

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

或者基本上是Objects.requireNotNull(yourReference)

1
此外,静态分析工具通常知道标准库中的 @NonNull 和 @Nullable,这也意味着它们能够检查空值(与显式检查空值相同)。
public static <T> @NonNull T requireNonNull(@Nullable T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

1
除了其他回答之外,对我来说使用requireNonNull可以使代码更加方便(有时易于阅读)。
例如-让我们检查下面的代码:
private int calculateStringLength(String input) {
        return Objects.
                requireNonNull(input, "input cannot be null").
                length();
    }

这段代码返回传递给它作为参数的字符串的长度 - 但是如果inputnull,它将抛出NPE。
如您所见,使用requireNonNull,就不需要再手动执行空检查了。
另一个有用的东西是“异常消息”是我自己编写的(在这种情况下是input不能为null)。

我喜欢旧代码中的这个想法,但是根据 链接,随着更现代的方法的存在,它也可能会使代码“不太易读”。 - aderchox

0

基本用法是立即检查并抛出NullPointerException

满足相同要求的更好选择(快捷方式)是使用Lombok的@NonNull注释。


1
注解并不是 Objects 方法的替代品,它们是相互配合的。你不能说 @ NonNull x=mightBeNull,你应该说 @ NonNull x=Objects.requireNonNull(mightBeNull, "inconceivable!"); - Bill K
@BillK 抱歉,我不理解你的意思。 - Supun Wijerathne
我只是在说Nonnull注释与requireNonNull一起使用,它不是一个替代品,但它们一起工作得非常好。 - Bill K
实现快速失败机制,这是一种替代方案,对吧?是的,我同意它不是作业的替代方案。 - Supun Wijerathne
我认为我会称requireNonNull()为从@ Nullable转换为@ NonNull的“转换”。 如果不使用注释,该方法实际上并不是非常有趣(因为它所保护的代码和它一样只会抛出NPE)-尽管它确实清楚地显示了您的意图。 - Bill K

0

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接