Guava checkNotNull 的作用是什么?

75

我对Guava还比较陌生(说实话,我不是“相当陌生”,在这方面我完全是新手),所以我决定阅读一些文档,并在阅读以下内容时感到非常惊讶:

com.google.common.base.Preconditions.checkNotNull(...)

我不明白这个方法的意义。这意味着不再进行以下操作:

myObject.getAnything();

(如果myObject为空,可能会引起NullPointerException)

我应该使用

checkNotNull(myObject).getAnything();

如果myObject为空,它将抛出一个NullPointerException,否则返回myObject

我很困惑,可能这是最愚蠢的问题,但是......

这有什么意义呢? 对于任何我能想到的情况,这两行代码都会产生相同的结果。

我甚至认为后者不太易读。

所以我一定是漏掉了什么。是什么呢?


2
我并不真正认同在每个地方都添加checkNotNull的想法。在我看来,如果您明确验证输入,而不是过度依赖帮助混淆代码的语法糖,那么这将更可取。快速失败没问题,但checkNotNull只会为故障排除提供任何其他信息-->您可以为错误的输入格式自己创建异常,并放入一个解释特定情况的好消息。 - Hoàng Long
我完全同意这是荒谬的。首先检查 null,然后自己抛出 NPE,在我看来完全疯了。至少应该抛出 ValidationException 或 IllegalArgumentException。 - Lawrence
考虑使用checkNotNull(reference, errorMessageTemplate, errorMessageArgs)提供更好的诊断:https://guava.dev/releases/23.0/api/docs/com/google/common/base/Preconditions.html#checkNotNull-T-java.lang.String-java.lang.Object...- - Vadzim
3个回答

110

这个想法是快速失败。比如,考虑这个可笑的类:

public class Foo {
    private final String s;

    public Foo(String s) {
        this.s = s;
    }

    public int getStringLength() {
        return s.length();
    }
}

假设你不想允许s的值为空(否则getStringLength将抛出NPE)。在当前这个类的情况下,当你捕获到null时,已经太晚了,很难找出是谁放进去的。罪魁祸首有可能在一个完全不同的类中,而且那个Foo实例可能构造很久以前。现在你必须梳理你的代码库,才能找出谁可能会把null值放在那里。

相反,想象一下这个构造函数:

public Foo(String s) {
    this.s = checkNotNull(s);
}

现在,如果有人在其中放了一个null,你会立即发现,并且你将会得到指向出错位置的堆栈跟踪。


另一个使用场景是如果您想在执行可能修改状态的操作之前检查参数。例如,考虑以下计算其所接收字符串长度平均值的类:

public class StringLengthAverager {
    private int stringsSeen;
    private int totalLengthSeen;

    public void accept(String s) {
        stringsSeen++;
        totalLengthSeen += s.length();
    }

    public double getAverageLength() {
        return ((double)totalLengthSeen) / stringsSeen;
    }
}

调用 accept(null) 会抛出 NPE,但在 stringsSeen 被递增之前不会抛出。这可能不是您想要的;作为该类的用户,我希望如果它不接受 null,则其状态应在传递 null 时保持不变(换句话说:调用应该失败,但不应使对象无效)。显然,在此示例中,您还可以通过在递增 stringsSeen 之前获取 s.length() 来修复它,但是对于更长或更复杂的方法,首先检查所有参数是否有效,然后再修改状态可能会更有用:

    public void accept(String s) {
        checkNotNull(s); // that is, s != null is a precondition of the method

        stringsSeen++;
        totalLengthSeen += s.length();
    }

1
这不应该更多吗? this.s = Preconditions.checkNotNull(s); - Ar3s
但是是的,我没有考虑过存储情况。 - Ar3s
13
典型的模式是 this.s = checkNotNull(s); 并伴随着一个静态导入。 - ColinD
1
个人认为应该避免这种错误报告。无论你做什么,NullPointerException 实际上几乎没有任何意义。我强烈反对在任何函数中传递/返回 null(除非你正在使用旧版 API),因此任何试图这样做的努力都应该得到 WrongApiUsage 异常的提示。 - Hoàng Long
3
我也希望 checkNotNull 会抛出 IllegalArgumentException 而不是 NPE。对我来说,NPE 意味着有人试图解引用一个空指针——而不是有人在尝试解引用之前就捕获了它为空的事实。但是,话虽如此,我认为“IllegalArgumentException:foo was null”在实际意义上并不比“NullPointerException:foo”更有用。因此,虽然我有点希望 Guava 的开发人员选择了 IllegalArgumentException,但是 checkNotNull 的方便性和标准化优先于这个小问题,至少对我来说是这样。 - yshavit
显示剩余2条评论

10

myObject.getAnything();(如果myObject为null可能会导致NullPointerException)

不,当myObject == null时它抛出NPE。在Java中,没有机会调用具有null接收器的方法(理论上的例外是静态方法,但它们可以且应该始终在没有任何对象的情况下调用)。


我应该使用checkNotNull(myObject).getAnything();

不,你不应该。这会显得相当冗余(更新)。

你应该使用checkNotNull以便进行快速失败。如果没有它,你可能会将非法的null传递给另一个方法,然后再继续传递下去,直到最终失败。然后你需要一些好运气才能发现实际上第一个方法应该拒绝null


yshavit的答案提到了一个重要的观点:传递非法值很糟糕,但存储它并稍后传递更糟。

更新

实际上,

 checkNotNull(myObject).getAnything()

这也是有道理的,因为你明确表达了不接受任何null值的意图。如果没有它,有人可能会认为你忘记了检查并将其转换为类似以下内容:

看起来也说得通,因为您清楚地表达了不接受任何空值的意图。没有它,某人可能会认为您忘记了检查并将其转换为以下内容:

 myObject != null ? myObject.getAnything() : somethingElse

另一方面,我认为这个检查并不值得冗长的描述。在更好的语言中,类型系统会考虑到空性,并为我们提供一些语义糖果,例如:

 myObject!!.getAnything()                    // checkNotNull
 myObject?.getAnything()                     // safe call else null
 myObject?.getAnything() ?: somethingElse    // safe call else somethingElse

对于可空的myObject,只有在已知myObject不为空时才允许使用标准点语法。


那就像在方法的开头检查方法的前置条件是否有效一样? - Juru
没错。立即检查非法参数可以确保您以后不必寻找它们。此外,每个查看该方法的人都可以看到什么是被禁止的(当然,应该有文档记录...)。 - maaartinus
@Juru:是的,这就是为什么它在“Preconditions”类中的原因! - ColinD

6
我几分钟前阅读了整个帖子,但是我对为什么要使用checkNotNull感到困惑。然后查看了Guava的Precondition类文档,得到了我期望的结果。过度使用checkNotNull肯定会降低性能。

我的想法是,在直接来自用户或最终API与用户交互的数据验证中,需要使用checkNotNull方法。它不应该在每个内部API方法中都使用,因为使用它无法停止异常,而是应该更正您的内部API以避免异常。

根据文档:链接

使用checkNotNull:

public static double sqrt(double value) {
     Preconditions.checkArgument(value >= 0.0, "negative value: %s", value);
     // calculate the square root
}

关于性能的警告

这个类的目标是提高代码的可读性,但在某些情况下,这可能会带来显著的性能成本。 请记住,消息构造的参数值必须全部计算出来,自动装箱和可变参数数组的创建也可能发生, 即使前置条件检查成功(在生产中应该几乎总是如此)。在某些情况下,这些浪费的CPU周期和分配可能会累加成为一个真正的问题。 对于性能敏感的前置条件检查可以始终转换为惯用形式:

if (value < 0.0) {
     throw new IllegalArgumentException("negative value: " + value);
}

1
抱歉,返回-1。我不建议在用户数据验证中使用此方法,因为您需要向用户提供更友好的非技术性错误消息。此外,如果可以预期出现异常情况,则不应使用运行时异常。用户可能会偶尔犯错。我建议在公共方法中检查方法参数,而不是在私有方法中检查。尽早捕获编程错误比过早进行优化更有价值。仅在发现实际存在性能问题时才替换前置条件检查。 - Henno Vermeulen
正如@HennoVermeulen所说的“在公共方法中检查方法参数”,我也说过“它不应该在每个内部API方法中使用”。谢谢。 - iamcrypticcoder
点赞提醒性能问题,但正如其他人所说,这不是一个很好的用户验证方法! - Tullochgorum

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