Java:如何有效地检查空指针

时间:2011-01-25 15:39:16

标签: java optimization coding-style nullpointerexception

有一些模式可用于检查方法的参数是否已赋予null值。

首先,经典之一。它在自制代码中很常见,并且显而易见。

public void method1(String arg) {
  if (arg == null) {
    throw new NullPointerException("arg");
  }
}

其次,您可以使用现有框架。该代码看起来更好一点,因为它只占用一行。缺点是它可能会调用另一个方法,可能使代码运行速度稍慢,具体取决于编译器。

public void method2(String arg) {
  Assert.notNull(arg, "arg");
}

第三,你可以尝试调用一个没有副作用的方法。这可能看起来很奇怪,但它的代币数比上述版本少。

public void method3(String arg) {
  arg.getClass();
}

我没有看到广泛使用的第三种模式,感觉就像我自己发明了它一样。我喜欢它的简洁性,因为编译器很有可能完全优化它或将其转换为单个机器指令。我还使用行号信息编译我的代码,因此如果抛出NullPointerException,我可以将其追溯到确切的变量,因为每行只有一个这样的检查。

您更喜欢哪种检查?为什么?

14 个答案:

答案 0 :(得分:22)

方法#3: arg.getClass();很聪明,但除非这个成语看到广泛采用,否则我更喜欢更清晰,更冗长的方法而不是保存几个字符。我是一个“写一次,读很多”的程序员。

其他方法是自我记录:可以使用日志消息来澄清发生的情况 - 在阅读代码时以及在运行时使用此日志消息。 arg.getClass(),就目前而言,并非自我记录。你可以使用评论至少o澄清代码的评论者:

arg.getClass(); // null check

但是你仍然没有机会像在其他方法中那样在运行时中放置特定的消息。


方法#1 vs#2(null-check + NPE / IAE vs assert): 我尝试遵循以下指南:

http://data.opengeo.org/GEOT-290810-1755-708.pdf

  • 使用assert检查私人方法的参数
    assert param > 0;

  • 使用空检查+ IllegalArgumentException检查公共方法的参数
    if (param == null) throw new IllegalArgumentException("param cannot be null");

  • 在需要的地方使用null check + NullPointerException
    if (getChild() == null) throw new NullPointerException("node must have children");


HOWEVER ,因为这个问题可能是关于最有效地捕捉潜在的null问题,那么我必须提到我处理{{{ 1}}正在使用静态分析,例如类型注释(例如null)a la JSR-305 。我最喜欢的检查工具是:

Checker Framework:
Java的自定义可插拔类型 https://checkerframework.org/manual/#checker-guarantees

如果是我的项目(例如,不是具有公共API的库),并且我可以在整个过程中使用Checker Framework:

  • 我可以在API中更清楚地记录我的意图(例如,此参数可能不是null(默认值),但是这个可能为null(@NonNull;方法可能返回null;等等)这个注释在声明中是正确的,而不是在Javadoc的更远处,因此更有可能被维护。

  • 静态分析比任何运行时检查更有效

  • 静态分析会提前标记潜在的逻辑缺陷(例如,我尝试将一个可能为null的变量传递给只接受非null参数的方法),而不是依赖于运行时发生的问题。

另一个好处是该工具允许我将注释放在注释中(例如`/ @Nullable /),因此我的库代码可以与类型注释的项目兼容,并且非类型 - 注释项目(不是我有任何这些)。


如果链接再次出现,请参阅GeoTools开发人员指南中的部分:

http://data.opengeo.org/GEOT-290810-1755-708.pdf

5.1.7使用断言,IllegalArgumentException和NPE

Java语言已经有几年了,现在可以使用断言关键字;此关键字可用于执行仅调试检查。 虽然此工具有多种用途,但常见的方法是检查私有(非公共)方法的方法参数。其他用途是 后置条件和不变量。

参考: Programming With Assertions

前置条件(如私有方法中的参数检查)通常是断言的简单目标。后期条件和不变量是有时 不那么直接但更有价值,因为非平凡的条件有更多的风险被打破。

  • 示例1:在引用模块中进行地图投影后,断言执行逆映射投影并检查结果 与原点(后置条件)。
  • 示例2:在DirectPosition.equals(Object)实现中,如果结果为true,则断言确保 hashCode()与Object契约的要求相同。

使用Assert检查私有方法上的参数

@Nullable

您可以使用以下命令行参数启用断言:

private double scale( int scaleDenominator ){
 assert scaleDenominator > 0;
 return 1 / (double) scaleDenominator;
}

您只能使用以下命令行参数转换GeoTools断言:

java -ea MyApp

您可以禁用特定包的断言,如下所示:

java -ea:org.geotools MyApp

使用IllegalArgumentExceptions检查公共方法上的参数

严格禁止在公共方法上使用断言;因为报告的错误是在客户代码中做出的 - 说实话 当他们搞砸了时​​,告诉他们前面有一个IllegalArgumentException。

java -ea:org.geotools -da:org.geotools.referencing MyApp

在需要时使用NullPointerException

如果可能,执行您自己的空检查;使用详细信息抛出IllegalArgumentException或NullPointerException 关于出了什么问题。

public double toScale( int scaleDenominator ){
 if( scaleDenominator > 0 ){
 throw new IllegalArgumentException( "scaleDenominator must be greater than 0");
 }
 return 1 / (double) scaleDenominator;
}

答案 1 :(得分:12)

你不是过早地优化biiiiiiiiiiiiit!?

我会先使用第一个。它简洁明了。

我很少使用Java,但我认为有一种方法可以让Assert仅在调试版本上运行,因此这将是禁止的。

第三个给了我毛骨悚然,我想如果我在代码中看到它,我会立即诉诸暴力。它完全不清楚它在做什么。

答案 2 :(得分:5)

你不应该抛出NullPointerException。如果你想要一个NullPointerException,只是不要检查该值,当参数为null并且你试图取消引用它时它将被自动抛出。

查看apache commons lang Validate和StringUtils类 Validate.notNull(variable)如果“variable”为null,它将抛出IllegalArgumentException 如果“variable”为空(null或零长度),Validate.notEmpty(variable)将抛出IllegalArgumentException。 也许更好:
String trimmedValue = StringUtils.trimToEmpty(variable)将保证“trimmedValue”永远不会为空。如果“variable”为null,则“trimmedValue”将为空字符串(“”)。

答案 3 :(得分:5)

您可以使用Objects Utility Class。

public void method1(String arg) {
  Objects.requireNonNull(arg);
}

请参阅http://docs.oracle.com/javase/7/docs/api/java/util/Objects.html#requireNonNull%28T%29

答案 4 :(得分:3)

第一种方法是我的偏好,因为它表达了最多的意图。通常可以在编程中使用快捷方式,但我认为较短的代码并不总是更好的代码。

答案 5 :(得分:3)

在我看来,第三种方法存在三个问题:

  1. 随意的读者不清楚这个意图。
  2. 即使您有行号信息,行号也会更改。在真实的生产系统中,知道第100行SomeClass出现问题并不能为您提供所需的全部信息。您还需要知道相关文件的修订版本,并能够获得该修订版本。总而言之,很多麻烦似乎都没什么好处。
  3. 一点也不清楚为什么你认为可以优化对arg.getClass的调用。这是一种本地方法。除非HotSpot被编码为具有针对这种确切可能性的方法的特定知识,否则它可能会单独留下调用,因为它无法知道被调用的C代码的任何潜在副作用。
  4. 我觉得每当我觉得需要进行空检查时,我会使用#1。在错误消息中使用变量名称非常适合快速找出究竟出错的地方。

    P.S。我不认为优化源文件中的令牌数量是一个非常有用的标准。

答案 6 :(得分:2)

x == null超快,它可以是几个CPU时钟(包括将成功的分支预测)。 AssertNotNull将被内联,因此没有区别。

x.getClass()不应该比x == null更快,即使它使用陷阱。 (原因:x将在某个寄存器中,并且检查寄存器与立即值的速度很快,分支也将被正确预测)

底线:除非你做了一些非常奇怪的事情,否则它将由JVM优化。

答案 7 :(得分:1)

第一个选项是最简单的选项,也是最清晰的选项。

在Java中并不常见,但在C和C ++中,=运算符可以包含在if语句的表达式中,从而导致错误,通常建议在变量和常量之间切换位置,如下所示: / p>

if (NULL == variable) {
   ...
}

而不是:

if (variable == NULL) {
   ...
}

防止类型错误:

if (variable = NULL) { // Assignment!
   ...
}

如果进行更改,编译器会为您找到这种错误。

答案 8 :(得分:1)

虽然我同意更喜欢避免getClass()hack的普遍共识,但值得注意的是,从OpenJDK版本1.8.0_121开始,javac将使用getClass()hack在创建lambda之前插入null检查表达式。例如,考虑:

public class NullCheck {
  public static void main(String[] args) {
    Object o = null;
    Runnable r = o::hashCode;
  }
}

使用javac编译后,可以通过运行javap -c NullCheck使用javap查看字节码。输出是(部分):

Compiled from "NullCheck.java"
public class NullCheck {
  public NullCheck();
    Code:
       0: aload_0
       1: invokespecial #1    // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: aconst_null
       1: astore_1
       2: aload_1
       3: dup
       4: invokevirtual #2    // Method java/lang/Object.getClass:()Ljava/lang/Class;
       7: pop
       8: invokedynamic #3, 0 // InvokeDynamic #0:run:(Ljava/lang/Object;)Ljava/lang/Runnable;
      13: astore_2
      14: return
}

“第3行,第4行和第7行”中的指令基本上是调用o.getClass(),并丢弃结果。如果你运行NullCheck,你将从第4行抛出NullPointerException。

这是否是Java人员得出的结论是必要的优化,或者它只是一个廉价的黑客,我不知道。然而,基于John Rose在https://bugs.openjdk.java.net/browse/JDK-8042127?focusedCommentId=13612451&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-13612451的评论,我怀疑可能确实存在这样的情况:产生隐式空检查的getClass()hack可能比其显式对应物更具性能。也就是说,我会避免使用它,除非仔细基准测试表明它产生了明显的差异。

(有趣的是,Eclipse Compiler For Java(ECJ)包含此null检查,并且运行由ECJ编译的NullCheck不会抛出n NPE。)

答案 9 :(得分:0)

我使用内置的Java断言机制。

assert arg != null;

这比其他所有方法的优势在于它可以关闭。

答案 10 :(得分:0)

我更喜欢方法4,5或6,其中#4适用于公共API方法,5/6适用于内部方法,但#6更常用于公共方法。

/**
 * Method 4.
 * @param arg A String that should have some method called upon it. Will be ignored if
 * null, empty or whitespace only.
 */
public void method4(String arg) {
   // commons stringutils
   if (StringUtils.isNotBlank(arg) {
       arg.trim();
   }
}

/**
 * Method 5.
 * @param arg A String that should have some method called upon it. Shouldn't be null.
 */
public void method5(String arg) {
   // Let NPE sort 'em out.
   arg.trim();
}

/**
 * Method 6.
 * @param arg A String that should have some method called upon it. Shouldn't be null.
 */
public void method5(String arg) {
   // use asserts, expect asserts to be enabled during dev-time, so that developers
   // that refuse to read the documentations get slapped on the wrist for still passing
   // null. Assert is a no-op if the -ae param is not passed to the jvm, so 0 overhead.

   assert arg != null : "Arg cannot be null"; // insert insult here.
   arg.trim();
}

处理空值的最佳解决方案是不使用空值。包装可能返回带有空值保护的空值的第三方或库方法,将值替换为有意义的值(例如空字符串),但在使用时不执行任何操作。如果确实不应该传递null,则抛出NPE,尤其是在不立即调用传递对象的setter方法中。

答案 11 :(得分:0)

这个没有投票,但是我使用了#2的轻微变体,比如

erStr += nullCheck (varName, String errMsg); // returns formatted error message

理由:(1)我可以遍历一堆参数,(2)nullCheck方法隐藏在超类中,(3)隐藏在循环的末尾,

if (erStr.length() > 0)
    // Send out complete error message to client
else
    // do stuff with variables

在超类方法中,你的#3看起来不错,但是我不会抛出异常(重点是什么,有人必须处理它,作为一个servlet容器,tomcat会忽略它,所以它也可以是这个()) 问候, - M.S。

答案 12 :(得分:0)

第一种方法。我永远不会做第二种或第三种方法,除非它们由底层JVM有效实现。否则,这两个只是过早优化的主要示例(第三个可能会有性能损失 - 您不希望在一般接入点处理和访问类元数据。)

NPE的问题在于它们是跨越编程的许多方面的东西(而我的方面,我的意思是更深刻和更深刻的AOP)。这是一个语言设计问题(并不是说语言很糟糕,但它是允许空指针或引用的任何语言的一个基本的短缺......)

因此,最好只在第一种方法中明确处理它。所有其他方法都是(失败)尝试简化操作模型,这是底层编程模型中不可避免的复杂性。

这是一个我们无法忍受咬的子弹。明确地处理它 - 在一般情况下 - 在路上不那么痛苦。

答案 13 :(得分:-1)

我相信第四个也是最有用的模式是什么都不做。您的代码将在稍后的几行中抛出NullPointerException或其他异常(如果null是非法值),并且如果在此上下文中为null,则可以正常工作。

我相信你只有在与它有关时才应该进行空检查。在大多数情况下,检查抛出异常是无关紧要的。 只是不要忘记在javadoc中提及参数是否为null。