如何更有效地使用@Nullable和@Nonnull注释?

179

我可以看到 @Nullable@Nonnull 注释可能会有助于防止NullPointerException,但它们的作用范围有限。

  • 这些注释的有效性在一级间接后完全降低,因此如果您仅添加了几个,它们的作用范围不会很远。
  • 由于这些注释没有得到很好的执行,因此有一个危险,即假定带有@Nonnull标记的值不为null,并因此不执行空检查。

下面的代码导致一个被标记为@Nonnull的参数为null,而不引发任何投诉。 当运行该代码时,它会抛出一个NullPointerException

public class Clazz {
    public static void main(String[] args){
        Clazz clazz = new Clazz();

        // this line raises a complaint with the IDE (IntelliJ 11)
        clazz.directPathToA(null);

        // this line does not
        clazz.indirectPathToA(null); 
    }

    public void indirectPathToA(Integer y){
        directPathToA(y);
    }

    public void directPathToA(@Nonnull Integer x){
        x.toString(); // do stuff to x        
    }
}

有没有办法使这些注释更严格执行和/或传播得更远?


1
我喜欢@Nullable@Nonnull的想法,但它们是否值得使用很可能会引发争议。 - Maarten Bodewes
我认为将来实现此举可能会导致编译器错误或警告,并要求在使用可空变量调用@Nonnull方法时强制转换为@Nonnull。 当然,在Java 7中无法使用注释进行转换,但是Java 8将增加将注释应用于变量的能力,包括强制转换。 因此,在Java 8中可能会实现这一点。 - Theodore Murdock
2
@TheodoreMurdock,是的,在Java 8中,语法上允许进行强制类型转换(@NonNull Integer) y,但编译器不允许基于注释发出任何特定的字节码。对于运行时断言,如在https://bugs.eclipse.org/442103中讨论的那样,小的帮助方法就足够了(例如,`directPathToA(assertNonNull(y))`)- 但请注意,这仅有助于快速失败。唯一安全的方法是执行实际的空检查(加上希望else分支中的替代实现)。 - Stephan Herrmann
3
请说明您所指的@Nonnull@Nullable是哪些,因为有多个类似的注释(请参见**此问题**)。您是在谈论包javax.annotation中的注释吗? - James Dunn
1
@TJamesBoone 对于这个问题的背景并不重要,重要的是如何有效地使用它们中的任何一个。 - Mike Rylander
显示剩余7条评论
11个回答

88

简短回答:我猜这些注释只对你的IDE有用,以警告可能出现的空指针错误。

正如《Clean Code》书中所说,你应该检查公共方法的参数,并避免检查不变量。

另一个好建议是永远不要返回null值,而是使用Null Object模式


19
对于可能为空的返回值,我强烈建议使用 Optional 类型而不是简单的 null - Patrick
12
可选类型并不比 "null" 更好。Optional#get() 会抛出 NoSuchElementException 异常,而使用 null 则会抛出 NullPointerException 异常。这两者都是 RuntimeException,没有明确的描述。我更喜欢可为空的变量。 - 30thh
8
为什么你会直接使用 Optional.get() 而不是先使用 Optional.isPresent() 或 Optional.map 呢? - GauravJ
17
为什么你会直接使用可为空的变量而不先检查它是否为空呢?;-) - 30thh
17
在这种情况下,“Optional”和可空类型的区别在于,“Optional”更好地传达了该值可以有意为空的信息。当然,它并不是一个魔术棒,在运行时它可能会像可空变量一样失败。然而,在我看来,“Optional”的API接收程序员更好。 - user1053510
显示剩余6条评论

39
除了IDE在您将null传递给期望参数不为null的方法时给出提示外,还有其他优点:
  • 静态代码分析工具可以测试与您的IDE相同的东西(例如FindBugs)
  • 您可以使用面向方面的编程(AOP)来检查这些断言
这可以帮助您的代码更易于维护(因为您不需要null检查),并且更少出错。

11
我理解OP的心情,因为尽管您提到了这两个优点,但在两种情况下,您都使用了“可以”这个词。这意味着不能保证这些检查实际上会发生。现在,这种行为上的区别可能对性能敏感的测试很有用,您可能希望避免在生产模式下运行这些测试,针对这种情况我们有“assertion(断言)”。我认为@Nullable@Nonnull是有用的概念,但我希望它们更有说服力,而不是我们对可以用它们做什么进行假设,这仍然可能什么都不做。 - seh
2
问题在于从哪里开始。目前他的注释是可选的。有时我希望它们不是这样,因为在某些情况下强制执行它们会很有帮助... - Uwe Plonus
请问您所提到的AOP是什么? - Chris.Zou
@Chris.Zou AOP指的是面向切面编程,例如AspectJ。 - Uwe Plonus

17
我认为这个原始问题间接地指出了一个普遍的建议,即仍然需要运行时空指针检查,即使使用了@NonNull。请参考以下链接: Java 8的新类型注释 在上面的博客中,建议如下:
可选类型注释不能替代运行时验证 在类型注释之前,描述诸如可空性或范围等内容的主要位置是在javadoc中。有了类型注释,这种通信以一种编译时验证的方式进入字节码。您的代码仍应执行运行时验证。

1
明白了,但默认的代码检查警告称运行时空值检查是不必要的,这似乎会让人对此建议感到不太鼓励。 - swooby
2
通常情况下,如果我确定我的代码是正确的,我会忽略lint警告。那些警告并不是错误。 - jonathanzh

13

在启用基于注解的null分析时,使用Eclipse编译原始示例并将兼容性设置为1.8会收到以下警告:

    directPathToA(y);
                  ^
Null type safety (type annotations): The expression of type 'Integer' needs unchecked conversion to conform to '@NonNull Integer'

这个警告类比于在使用原始类型(“unchecked conversion”)混合泛型代码和旧代码时收到的警告。我们在这里也有完全相同的情况:方法indirectPathToA()具有“旧的”签名,因为它没有指定任何空值约定。工具可以轻松报告此问题,因此它们将追踪所有需要传播但尚未传播空注释的地方。

当使用聪明的@NonNullByDefault时,我们甚至不必每次都说这些内容。

换句话说,是否“传播很远”的空注释取决于您使用的工具以及您对工具发出的所有警告的严谨程度。通过TYPE_USE null annotations,您最终可以选择让工具警告您程序中每一个可能的NPE,因为空值已成为类型系统的内在属性。


8
在我的项目中,我会在“常量条件和异常”代码检查中激活以下选项:为可能返回null的方法建议@Nullable注释,并报告传递给未注释参数的可空值Inspections。当激活时,所有未注释的参数将被视为非null,因此您还将在间接调用时看到警告:
clazz.indirectPathToA(null); 

如果需要更加严格的检查,Checker Framework 可能是一个不错的选择(可以看看这个不错的 教程)。 :我还没有使用过它,可能会存在与 Jack 编译器的问题,请查看 这个 bug 报告


这样干净多了! - vintprox

7

如果您使用Kotlin,它在编译器中支持这些空值注释,并会阻止您将null传递给需要非空参数的Java方法。尽管这个问题最初是针对Java的,但我提到这个Kotlin功能是因为它专门针对这些Java注释,并且问题是“有没有办法使这些注释更严格执行和/或传播更远?”而且这个功能确实使这些注释更严格执行。

使用@NotNull注释的Java类:

public class MyJavaClazz {
    public void foo(@NotNull String myString) {
        // will result in an NPE if myString is null
        myString.hashCode();
    }
}

Kotlin类调用Java类,并将带有@NotNull注解的参数设置为null:
class MyKotlinClazz {
    fun foo() {
        MyJavaClazz().foo(null)
    }
}  

Kotlin编译器错误强制执行@NotNull注解:
错误:(5, 27)Kotlin:Null不能是非空类型String的值
请参阅:https://kotlinlang.org/docs/java-interop.html#nullability-annotations

3
这个问题涉及到Java,根据它的第一个标签,而不是Kotlin。 - seh
2
请查看更新,了解为什么这个答案与此问题相关。 - Mike Rylander
2
好的,这是 Kotlin 的一个不错的特性。但我认为它并不能满足那些来这里学习 Java 的人。 - seh
但是,即使在参数中没有添加@NotNull,访问myString.hashCode()仍然会抛出NPE。那么添加它有什么更具体的作用呢? - kAmol
@kAmol 这里的区别在于使用 Kotlin 时,您将获得一个编译时错误而不是运行时错误。注释是为了通知开发人员需要确保不传递 null。这不会阻止在运行时传递 null,但它将防止您编写调用此方法的代码与 null(或可以返回 null 的函数)一起的代码。 - Mike Rylander

7

我同意注释“不会传播很远”的说法。然而,我认为这是程序员的错误。

我将Nonnull注释理解为文档。以下方法表达了它需要(作为前提条件)非空参数x

    public void directPathToA(@Nonnull Integer x){
        x.toString(); // do stuff to x        
    }

以下代码片段存在一个错误。该方法调用directPathToA()而没有强制要求y为非空(也就是说,它并没有保证被调用的方法的前提条件)。一种可能性是在indirectPathToA()中添加Nonnull注释(传播前提条件)。第二种可能性是在indirectPathToA()中检查y的空值,并在y为空时避免调用directPathToA()
    public void indirectPathToA(Integer y){
        directPathToA(y);
    }

1
@Nonnull传播到indirectPathToA(@Nonnull Integer y)是我个人认为不好的做法:您需要在整个调用堆栈上维护传播(因此,如果您在directPathToA()中添加了一个null检查,您需要在整个调用堆栈中用@Nullable替换@Nonnull)。对于大型应用程序来说,这将是巨大的维护工作。 - Julien Kronegg
3
@Nonnull注释只是强调参数的null验证在您这一方面(您必须保证传递非空值)。这不是方法的责任。 - Alexander Drobyshevsky
1
@Nonnull 在这个方法中,当空值没有任何意义时也是一个明智的选择。 - Alexander Drobyshevsky

4

在Java中,我会使用Guava的Optional类型。作为一个实际的类型,您可以获得编译器关于其使用的保证。虽然很容易绕过它并获得NullPointerException,但至少该方法的签名清楚地传达了它期望的参数或可能返回的内容。


17
你必须小心对待这个问题。Optional 应该只在一个值确实是可选的情况下使用,而其缺失作为后续逻辑的决策门槛。我曾经看到过人滥用 Optional ,将所有 Object 替换成了 Optional ,同时将 null 检查替换成了对值存在性的检查,这种做法是误解了 Optional 的本意。 - Christopher Perry
1
如果您的目标是JDK 8或更高版本,请优先使用java.util.Optional而不是Guava的类。有关差异的详细信息,请参见Guava的注释/比较。 - AndrewF
1
“null检查被替换为存在性检查,这忽略了重点”,那么您能详细说明一下这个重点是什么吗?在我看来,这不是Optionals的唯一原因,但肯定是迄今为止最大和最好的原因。 - scubbo

0
一个非常老的问题,但我没有看到任何人指出要获取执行时间检查应该使用assertions。
就像这样
assert x != null;

无论间接级别如何,这都将起作用(抛出异常)


这是正确的,但 NPE 已经被抛出了... 我最终选择的答案是尽可能使用 Kotlin,然后将其作为编译时检查。请参见:https://dev59.com/jGYr5IYBdhLWcg3wq8CI#46290602 - Mike Rylander

0

来自javax.validation.constraints的@NotNull表示该方法永远不会返回null值.. ;) 这意味着在从该方法获取任何值时,您无需添加任何空指针异常的空检查.. :D


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