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

有一些模式用于检查方法的参数是否已被赋予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 ,我可以将其追溯到确切的变量,因为每行只有一个这样的检查。

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

方法#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;

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

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


但是 ,由于这个问题可能是关于最有效地捕获潜在的null问题,所以我不得不提到我处理null首选方法是使用静态分析,例如类型注释(例如@NonNull )和JSR-305 。 我最喜欢检查它们的工具是:

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

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

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

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

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

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


如果链接再次死亡 ,这里是GeoTools开发人员指南中的部分:

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

5.1.7使用断言,IllegalArgumentException和NPE

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

参考: 使用断言编程

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

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

使用Assert检查Private方法上的参数

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

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

 java -ea MyApp 

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

 java -ea:org.geotools MyApp 

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

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

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

严格禁止在公共方法上使用断言; 因为报告的错误是在客户端代码中进行的 – 说实话,当他们搞砸了时​​就告诉他们IllegalArgumentException。

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

在需要时使用NullPointerException

如果可能,执行您自己的空检查; 抛出IllegalArgumentException或NullPointerException以及有关出错的详细信息。

 public double toScale( Integer scaleDenominator ){ if( scaleDenominator == null ){ throw new NullPointerException( "scaleDenominator must be provided"); } if( scaleDenominator > 0 ){ throw new IllegalArgumentException( "scaleDenominator must be greater than 0"); } return 1 / (double) scaleDenominator; } 

你不是过早地优化biiiiiiiiiiiiit !?

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

我很少使用Java,但我认为有一种方法可以让Assert只在调试版本上运行,所以这是一个禁忌。

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

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

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

您可以使用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

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

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

  1. 对于随意的读者来说,目的并不明确。
  2. 即使您有行号信息,行号也会改变。 在真实的生产系统中,知道第100行的SomeClass中存在问题并不能为您提供所需的所有信息。 您还需要知道相关文件的修订版本并能够获得该修订版本。 总而言之,很多麻烦似乎没什么好处。
  3. 你完全不清楚为什么你认为可以优化对arg.getClass的调用。 这是一种本地方法。 除非HotSpot被编码为具有针对这种确切可能性的方法的特定知识,否则它可能会单独留下调用,因为它无法知道被调用的C代码的任何潜在副作用。

我喜欢每当我觉得需要空检查时使用#1。 在错误消息中使用变量名称非常适合快速找出究竟出错的地方。

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

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

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

结论:除非你做一些真正奇怪的事情,否则它将由JVM优化。

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

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

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

代替:

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

防止类型错误:

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

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

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

 assert arg != null; 

与其他所有方法相比,它的优点是可以关闭它。

我更喜欢方法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方法中,传递的对象不会立即被调用。

这个没有投票,但我使用#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看起来不错,但我不会抛出exception(重点是什么,有人必须处理它,作为一个servlet容器,tomcat会忽略它,所以它也可能是这个( ))问候, – MS

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

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

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

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

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

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

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

 Compiled from "NullCheck.java" public class NullCheck { public NullCheck(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()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不会抛出NPE。)

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

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