【问题标题】:Why should one use Objects.requireNonNull()?为什么要使用 Objects.requireNonNull()?
【发布时间】:2018-01-19 20:05:00
【问题描述】:

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

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

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

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

【问题讨论】:

标签: java java-8 nullpointerexception


【解决方案1】:

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

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

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

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

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

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

【讨论】:

  • 我喜欢旧代码中的这个想法,但是根据link,随着更现代方法的存在,它也可能使代码“可读性降低”。
【解决方案2】:

稍后访问null 对象的成员时会引发空指针异常。 Objects.requireNonNull() 立即检查该值并立即抛出异常,而无需继续前进,并且更容易检测到实际发生空异常的位置。

【讨论】:

    【解决方案3】:

    除了所有正确答案:

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

    只是一个例子:想象一下你有

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

    其中parseAndValidaterequireNonNull 中的NullPointerException 包装在ParsingException 中。

    现在您可以决定,例如何时重试:

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

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

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

    现在RetryExhaustException 将终止您的订阅。

    【讨论】:

      【解决方案4】:

      在实现可空性检查的编译器扩展的上下文中(例如:uber/NullAway),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));
      

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          【解决方案6】:

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

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

          【讨论】:

          • 注解不是 Objects 方法的替代品,它们一起工作。你不能说@NonNull x=mightBeNull,你会说@NonNull x=Objects.requireNonNull(mightBeNull, "不可思议!");
          • @BillK 对不起,我不明白你
          • 我只是说 Nonnull 注释与 requireNonNull 一起工作,它不是替代品,但它们可以很好地协同工作。
          • 实现快速失败的性质,它是另一种选择,对吗?是的,我同意它不是任务的替代品。
          • 我想我将 requireNonNull() 称为从@Nullable 到@NonNull 的“转换”。如果您不使用注释,那么该方法就不是很有趣(因为它所做的只是抛出一个 NPE,就像它保护的代码一样)——尽管它确实很清楚地表明了您的意图。
          【解决方案7】:

          使用requireNonNull() 作为方法中的第一条语句可以立即/快速识别异常的原因。
          堆栈跟踪清楚地表明,一旦进入方法,就会引发异常因为调用者不遵守要求/合同。null 对象传递给另一个方法可能确实会一次引发异常,但问题的原因可能更难以理解,因为异常将在@987654323 的特定调用中引发@ 可能更远的对象。


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

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

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

          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 参数提供 null 引用:

          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); 
          
          线程“主”java.lang.NullPointerException 中的异常 在 LookupService.isFirstElement(LookupService.java:5) 在 Dictionary.isFirstElement(Dictionary.java:15) 在 Dictionary.main(Dictionary.java:22)

          异常是在LookupService 类中触发的,而它的起源要早得多(Dictionary 构造函数)。它使整体问题分析变得不那么明显。
          wordsnull?是words.get(0) null 吗?两个都 ? 为什么一个、另一个或两者都是 nullDictionary(构造函数?调用方法?)中的编码错误?是 LookupService 中的编码错误吗? (构造函数?调用的方法?)?
          最后,我们将不得不检查更多代码以找到错误来源,并且在更复杂的类中甚至可能使用调试器来更容易地理解发生了什么。
          但是为什么一件简单的事情(缺少空检查)会变成一个复杂的问题呢?
          因为我们允许在较低组件上泄漏特定组件上的初始错误/缺失。
          想象一下 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");
          
          线程“主”java.lang.NullPointerException 中的异常 在 java.util.Objects.requireNonNull(Objects.java:203) 在 com.Dictionary.(Dictionary.java:15) 在 com.Dictionary.main(Dictionary.java:24)

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

          【讨论】:

          • 哇!很好的解释。
          • 我同意@JonathasNascimento,很好的解释和使用示例。
          【解决方案8】:

          快速失败

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

          这通常称为“提前失败”或"fail-fast"

          【讨论】:

            【解决方案9】:

            因为您可以通过这样做使事情显式。喜欢:

            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 不为空 - 因此您不需要在其他地方进行任何if (bar == null) 检查!

            【讨论】:

            • 你可以写this.bar = Objects.requireNonNull(bar, "bar must not be null");让你的代码更紧凑
            • @KirillRakhman 教 GhostCat 一些我不知道的很酷的东西 --> 在 GhostCat 投票中赢得彩票。
            • 我认为这里的评论 #1 是Objects.requireNonNull(bar, "bar must not be null"); 的实际好处。谢谢这个。
            • 哪个是第一个,为什么“语言”的人要关注 some 库的作者?!为什么番石榴不遵循 Java 语言行为?!
            • this.bar = Objects.requireNonNull(bar, "bar must not be null"); 在构造函数中是可以的,但如果在同一个方法中设置两个或多个变量,则在其他方法中可能存在危险。示例:talkwards.com/2018/11/03/…
            【解决方案10】:

            附带说明一下,在 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 - 如果 yourReference 是 null,它将失败。

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

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

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

            【讨论】:

              【解决方案11】:

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

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

              考虑:

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

              .NET 通过将NullReferenceException(“您取消引用了一个空值”)与ArgumentNullException(“您不应该将 null 作为参数传递 - 它是为了 this em> 参数)。我希望 Java 也能做同样的事情,但即使只有 NullPointerException,如果在最早可能出现的点抛出错误,修复代码仍然要容易得多检测到。

              【讨论】:

                猜你喜欢
                • 2020-10-02
                • 1970-01-01
                • 2017-05-16
                • 2012-08-22
                • 2016-09-12
                • 1970-01-01
                • 2020-11-07
                • 2011-07-27
                • 2020-05-05
                相关资源
                最近更新 更多