我应该为我的所有方法编写空检查吗?

Should I write null checks for all of my methods?

我正在创建自己的库,例如 Apache Commons DigestUtils 以供学习之用。

在我的 update() 方法中,我写了一个简单的 if (something == null) throw new Exception 作为 null 检查。

/**
 * Performs a digest update for the {@code String} (converted to bytes using the
 * UTF-8 standard charsets) with a specific {@code MessageDigest} and returns
 * the final digest data.
 *
 *
 * @param messageDigest the {@code MessageDigest} with a specific algorithm to
 *                      process the digest.
 *
 * @param data          the data to digest.
 *
 * @return the {@code MessageDigest} with the processed update for the digest.
 *
 * @throws IllegalArgumentException if {@code messageDigest} is {@code null}.
 *
 * @throws IllegalArgumentException if {@code data} is {@code null}.
 */
public static MessageDigest update(final MessageDigest messageDigest, final String data) {
    if (messageDigest == null)
        throw new IllegalArgumentException("messageDigest cannot be null");
    if (data == null)
        throw new IllegalArgumentException("data cannot be null");

    messageDigest.update(data.getBytes(StandardCharsets.UTF_8));

    return messageDigest;
}

现在我不确定是否应该在其他必须调用update()方法的方法中写相同的检查,或者只在注释中写该方法抛出相应的异常。

public static byte[] digest(final MessageDigest messageDigest, final byte[] data) {
    return update(messageDigest, data).digest();
}

public static byte[] digest(final MessageDigest messageDigest, final byte[] data) {
    if (messageDigest == null)
        throw new IllegalArgumentException("messageDigest cannot be null");
    if (data == null)
        throw new IllegalArgumentException("data cannot be null");

    return update(messageDigest, data).digest();
}

注意 digest() 方法调用 update() 方法。

最佳做法是什么?

最好的做法是让 update() 方法执行它的操作,如果它抛出 Exception,您也会抛出异常。有两个常见的事情要做,都抛出 Exception 你也得到一个。在 SomeException 所在的方法名称中放置 throws SomeException 的一种方法是您要排除的 Exception

public static byte[] digest(final MessageDigest messageDigest, final byte[] data) throws IllegalArgumentException {
    return update(messageDigest, data).digest();
}

所以如果你从update()得到一个Exception,它会自动抛出它。注意:您不需要抛出 IllegalArgumentException,因为它们是 RunTimeException 的一种类型,会自动抛出。第二种方法是 try catch,它给了你更多的选择。

public static byte[] digest(final MessageDigest messageDigest, final byte[] data) throws IllegalArgumentException {
    try {
        return update(messageDigest, data).digest();
    } catch(IllegalArgumentException e) {
        // Do whatever you want. You can even throw e if you want.
    }
}

这允许您在发现异常时为所欲为。

  1. 从 JDK15 开始(或者它已经在 14 中了吗?),系统本身抛出的 NPE(即当你 运行,比如说,Object o = null; o.hashCode(); 将拥有所有细节;特别是表达式的文本。这意味着如果在您的代码中会出现 NPE 'naturally'(因为您取消引用该变量,或将其传递给另一个始终这样做的方法),则不需要空检查。从风格上讲,问题是如果稍后您更改代码以使其不一定取消引用,您将失去调用该方法将抛出空参数的效果。需要记住的事情。

  2. 根据我的经验,更常见的是显式抛出 'NullPointerException',即写成 if (param == null) throw new NullPointerException("param")。自动抛出这个的各种库通常也默认为 NPE。

  3. 你不需要写if。 java 核心附带了一个单行实用程序:Objects.requireNonNull(param, "param"); 比你的 3 行 if 块短很多!

  4. 您可以使用 lombok's @NonNull 自动生成这些空检查,节省您更多的输入。

  5. 您可以将 IDE 配置为在更改参数值时发出警告;这意味着您不再需要在源文件中放置关键字 'final' 15,950,951(近似)次。节省您很多的打字时间。

  6. 虽然到目前为止这个问题的另一个答案说你不需要需要声明throws IllegalArgumentException,这是真的,这样做出于文档目的(同样,我的经验)很常见。

  7. 有标注非无效的注解。除了 lombok 之外,还有各种库可以为您提供编译时空值检查,它们通常内置于主要的 IDE 中,或者有一个插件,因此您会在编写时遇到错误您正在将(潜在的)空值传递给不需要它的方法,或者进行无用的空值检查(例如值 returned 来自表明它们从不 return null 的方法的空值检查)。如果您可以依靠图书馆的所有用户使用此类工具,那么您根本不需要 运行time nullchecks,但我建议您保留它们;我认为你不能指望你的用户拥有这些工具。不过,请考虑注释您的参数,以便那些确实受益于编译时检查的人。

对于编译时注释,这里有多种选择(不幸的是):checkerframework which is by far the most expansive, letting you go a lot further than compile-time null check guards. There's also the baked-into-your-IDE sets: eclipse's and intellij 是最常见的,但还有更多。

只有 lombok 会为你生成一个运行time nullcheck,并且IDEs 也可以配置为考虑将其用于编译时分析,让您两全其美。或者,将这些框架的注释之一与 Objects.requireNonNull.

结合起来

1.: 有函数Objects.requireNonNull,就是为了这个目的。如果给定的参数等于 null.

,它会抛出异常
if (messageDigest == null)
        throw new IllegalArgumentException("messageDigest cannot be null");

应该只是

Objects.requireNonNull(messageDigest, "messageDigest cannot be null");

2.: 由于 null 而抛出异常的最佳实践是将其抛出 null 不应该的地方t 出现在第一位 。这就是 Fail Fast Principle.

背后的动机

null 引用出现在它 不应该 的位置时,您想在 恰好 那个地方。您不想null 引用传递给其他methods/functions。否则,当传递的 null 引用导致您的程序出现问题时,您将很难调试代码。这是因为不清楚 null 引用首先出现在哪里;但实际上 造成的 正是你想要的 fix/handle。一个例子:

public class Demo {
    public void a(Object obj) {
        b(obj);
    }
    private void b(Object obj) {
        c(obj);
    }
    private void c(Object obj) {
        // Calculation using obj.
    }
}

如果很明显 a 永远不应该传递 null 引用,那么这是检查它的正确位置。

public void a(Object obj) {
    Objects.requireNonNull(obj); // Throw exception when null.
    b(obj);
}

在每个方法中都进行 null 检查是很糟糕的,因为 obj 不应该 null 这一事实对所有这些方法都很重要,因此足以放置null 首先检查 null 不应出现的位置,即 a。该程序应快速失败

null 检查放在 c 中会更糟糕,因为 c 逻辑上应该永远不会收到来自 bnull 引用,并且b 不应该从 a 收到一个。如果 null 引用导致 c 出现问题,您将很难确定 null 是否出现在 ba 或调用 a。您想要 知道,因为您想要修复发生的 null 引用的问题,而不是 它引起的问题。当然,有时候事件本身并不能解决问题,但是可以尽量接近事件发生,减少它造成的附带"damage"

没有真正的 goto 方法来放置 null 检查。您必须将它们放在正确的位置,以使程序在出现无效对象时 快速失败 null 检查的最佳位置由您决定,因为这完全取决于具体情况。

我希望这能让您对如何处理这个问题有个好主意。