为什么要使用 Objects.requireNonNull()?

Why should one use Objects.requireNonNull()?

提问人:user4686046 提问时间:8/11/2017 最后编辑:Salehuser4686046 更新时间:7/12/2023 访问量:213668

问:

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

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

但是,如果对象被取消引用,则无论如何都会被抛出。那么,为什么要做这个额外的 null 检查和抛出呢?NullPointerExceptionnullNullPointerException

一个显而易见的答案(或好处)是它使代码更具可读性,我同意。我很想知道在方法开始时使用的任何其他原因。Objects.requireNonNull()

java-8 nullpointerexception

评论

44赞 luk2302 8/11/2017
en.wikipedia.org/wiki/Fail-fast
5赞 Hulk 8/11/2017
为什么显式抛出 NullPointerException 而不是让它自然发生?
4赞 xorcus 4/11/2020
这种参数检查方法让你在编写单元测试时作弊。Spring 也有这样的实用程序(参见 docs.spring.io/spring/docs/current/javadoc-api/org/... )。如果你有一个“if”,并且想要有较高的单元测试覆盖率,你必须涵盖两个分支:当条件满足时,当条件不满足时。如果您使用 ,您的代码没有分支,因此单元测试的单次通过将使您获得 100% 的覆盖率 :-)Objects.requireNonNull
0赞 Johannes Kuhn 11/4/2020
托克托
0赞 javing 1/14/2021
下面提供的所有解释都是有效的,但在我看来,我会研究为什么该方法首先收到 null。我认为防御性地检查输入是否为 null 不是好的做法,在我看来,最好确保调用者不传递 null。

答:

397赞 GhostCat 8/11/2017 #1

因为你可以通过这样做使事情变得明确。喜欢:

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()
  • 保证bar 字段为非 null。

相比之下:你今天创建一个 Foo 对象,明天你调用一个使用该字段并抛出的方法。最有可能的是,你明天不会知道为什么昨天当它传递给构造函数时该引用为 null!

换言之:通过显式使用此方法检查传入的引用,可以控制引发异常的时间点。大多数时候,你希望尽可能快地失败

主要优点是:

  • 如前所述,受控行为
  • 更轻松的调试 - 因为您在对象创建上下文中抛出。在某个时间点,您的日志/跟踪有一定的机会告诉您出了什么问题!
  • 如上图所示:这个想法的真正力量与最终领域一起展开。因为现在你的类中的任何其他代码都可以安全地假设它不是 null - 因此你不需要在其他地方进行任何检查!barif (bar == null)

评论

50赞 Kirill Rakhman 8/15/2017
你可以通过编写使你的代码更紧凑this.bar = Objects.requireNonNull(bar, "bar must not be null");
6赞 GhostCat 8/15/2017
@KirillRakhman 教 GhostCat 一些我不知道的很酷的事情——>在 GhostCat 点赞彩票中赢得一张彩票。
2赞 Kawu 12/4/2018
我认为这里的评论#1是.谢谢你。Objects.requireNonNull(bar, "bar must not be null");
6赞 Erk 7/16/2019
这在构造函数中是可以的,但如果在同一方法中设置了两个或多个变量,则在其他方法中可能存在危险。示例:talkwards.com/2018/11/03/...this.bar = Objects.requireNonNull(bar, "bar must not be null");
4赞 maxime.bochon 9/7/2020
由于是静态方法,因此可以进行静态导入以使代码更短且更易于阅读: .Objects.requireNonNullthis.bar = requireNonNull(...)
177赞 luk2302 8/11/2017 #2

快速故障

代码应尽快崩溃。它不应该完成一半的工作,取消引用 null 然后才崩溃,留下一半的工作完成,导致系统处于无效状态。

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

53赞 Jon Skeet 8/11/2017 #3

但是,如果取消引用 null 对象,则无论如何都会引发 NullPointerException。那么,为什么要进行这个额外的 null 检查并抛出 NullPointerException?

这意味着您可以立即可靠地检测到问题。

考虑:

  • 在代码已经执行了一些副作用之后,在方法的后面才可能使用该引用
  • 在此方法中,引用可能根本无法取消引用
    • 它可以传递给完全不同的代码(即原因和错误在代码空间中相距甚远)
    • 它可以在很久以后使用(即原因和错误在时间上相距甚远)
  • 它可能用于 null 引用有效但具有意外效果的任何地方

.NET 通过将 (“你取消了引用 null 值”) 与 (“你不应该将 null 作为参数传入 - 它是针对此参数的 ) 分开来改进这一点。我希望 Java 做同样的事情,但即使只有一个 ,如果错误在可以检测到的最早点抛出,修复代码仍然容易得多NullReferenceExceptionArgumentNullExceptionNullPointerException

28赞 davidxxx 8/11/2017 #4

在方法中使用 as first 语句可以立即/快速识别异常原因。
堆栈跟踪清楚地表明,由于调用方不遵守要求/协定,因此在方法输入后立即引发异常。将对象传递给另一个方法确实可能一次引发异常,但问题的原因可能更难以理解,因为异常将在对对象的特定调用中引发,这可能会更远。
requireNonNull()nullnull


这是一个具体而真实的例子,它说明了为什么我们必须支持快速失败,更具体地说,使用或任何方式对设计为 not 的参数执行无空检查。Object.requireNonNull()null

假设一个类,它由 a 和 a 组成,表示 中包含的单词。这些字段被设计为不是,其中一个字段在构造函数中传递。DictionaryLookupServiceListStringnullDictionary

现在假设在方法条目中没有检查的“错误”实现(这里是构造函数):Dictionarynull

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

现在,让我们使用参数的引用调用构造函数:Dictionarynullwords

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)

异常是在类中触发的,而它的起源要早得多(构造函数)。它使整体问题分析变得不那么明显。
是?是?双? 为什么一个,另一个或两个? 是(构造函数? 调用的方法?)中的编码错误吗?是编码错误吗?(构造函数? 调用的方法??
最后,我们将不得不检查更多的代码来找到错误的根源,在更复杂的类中,甚至可以使用调试器来更容易地理解它发生了什么。
但是,为什么一件简单的事情(缺少空检查)变成了一个复杂的问题呢?
因为我们允许在特定组件上识别初始错误/缺失,从而在较低组件上泄漏。
想象一下,这不是一个本地服务,而是一个远程服务或调试信息很少的第三方库,或者想象一下,在检测到之前,你没有 2 层,而是 4 或 5 层对象调用?这个问题的分析将更加复杂。
LookupServiceDictionarywordsnullwords.get(0) nullnullDictionaryLookupServiceLookupServicenull

所以偏爱的方式是:

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)

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

评论

2赞 Jonathas Nascimento 11/1/2019
哇!很好的解释。
2赞 MarkyMarksFunkyBunch 8/11/2020
我同意@JonathasNascimento,非常好的解释和示例的使用。
2赞 Eugene 8/12/2017 #5

顺便说一句,在java-9之前,在一些jre类本身中实现的这种快速失败略有不同。假设情况:Object#requireNotNull

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

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

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

基本上是一个操作: - 如果 yourRefercence 是 ,这将失败。yourReference.getClassnull

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

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

或者基本上Objects.requireNotNull (yourReference)

0赞 Supun Wijerathne 7/18/2019 #6

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

满足相同要求的一个更好的选择(快捷方式)是 lombok @NonNull注释。

评论

1赞 Bill K 8/28/2019
批注不是 Objects 方法的替代品,它们协同工作。你不能说@NonNull x=mightBeNull,你会说@NonNull x=Objects.requireNonNull(mightBeNull, “不可思议!”);
0赞 Supun Wijerathne 8/28/2019
@BillK对不起,我不明白
0赞 Bill K 8/29/2019
我只是说 Nonnull 注释与 requireNonNull 一起使用,它不是替代方案,但它们可以很好地协同工作。
0赞 Supun Wijerathne 8/29/2019
要实现快速故障的性质,它是一种替代方法,对吧?是的,我同意它不是任务的替代方案。
0赞 Bill K 8/30/2019
我想我会将requireNonNull()称为从@ Nullable到@ NonNull的“转换”。如果你不使用注解,这个方法就不是很有趣了(因为它所做的只是抛出一个NPE,就像它保护的代码一样)——尽管它确实非常清楚地显示了你的意图。
17赞 Mirwise Khan 12/7/2019 #7

NPE(空指针异常)在稍后访问对象的成员时引发。 从访问值的位置回溯到初始化为 null 的位置,从而允许专注于 NPE 的真正来源。nullObjects.requireNonNull()

最简单的场景:

// Global String
String nullString = null;

调用 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赞 Violet Giraffe 8/9/2022
我将重新表述:它会立即指示 null 值发生的位置和时间,而不是首次访问此值的时间(使用 NPE)。
0赞 Andy 2/1/2020 #8

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

0赞 imoverclocked 6/30/2020 #9

在实现可为空性检查的编译器扩展的上下文中(例如:uber/NullAway),当您碰巧知道在代码中的某个点上有一个可为 null 的字段时,应谨慎使用。Objects.requireNonNull

这样,主要有两种用法:

  • 验证

    • 此处的其他回复已经涵盖
    • 具有开销和潜在 NPE 的运行时检查
  • 可空性标记(从 @Nullable 更改为 @Nonnull)

    • 最少地使用运行时检查,支持编译时检查
    • 仅当注释正确时才有效(由编译器强制执行)

可空性标记的用法示例:

@Nullable
Foo getFoo(boolean getNull) { return getNull ? null : new Foo(); }

// Changes contract from Nullable to Nonnull without compiler error
@Nonnull Foo myFoo = Objects.requireNonNull(getFoo(false));
0赞 PeMa 6/30/2020 #10

除了所有正确答案:

我们在反应式流中使用它。通常,生成的 s 会根据它们在流中的出现情况包装到其他 Exception 中。因此,我们以后可以轻松决定如何处理错误。NullpointerException

举个例子:想象一下你有

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

其中 wrapps 在 .parseAndValidateNullPointerExceptionrequireNonNullParsingException

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

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

如果不进行检查,方法中将发生 NullPointerException,这将导致无休止的重试。甚至值得,想象一下长期订阅,添加save

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

现在将终止您的订阅。RetryExhaustException

1赞 AADProgramming 10/2/2020 #11

除了其他答案 - 对我来说,使用 可以使代码变得有点方便,(有时易于阅读)requireNonNull

例如 - 让我们检查下面的代码,

private int calculateStringLength(String input) {
        return Objects.
                requireNonNull(input, "input cannot be null").
                length();
    }

此代码返回作为参数传递给它的字符串的长度 - 但是,如果 是 ,它将抛出 NPE。inputnull

如您所见,使用 - 没有理由再手动执行 null 检查。requireNonNull

另一个有用的东西是“异常消息”是我自己手写的(在本例中)。input cannot be null

评论

0赞 aderchox 9/27/2021
我喜欢旧代码中的这个想法,但随着更现代方法的存在,它也可能使代码“可读性降低”。
1赞 cquezel 3/6/2022 #12

还有一个额外的好处是,静态分析工具通常知道标准库的@NonNull和@Nullable(这与显式检查 null 相同):

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