一.简述

  • 通常在引用和集合中处理null变量非常棘手。 他们不仅很难被发现,而且处理起来也相对麻烦。
  • 实际上,在编译代码的时候是无法识别null将导致的任何问题,并在运行程序的时候抛出NullPointerException
  • 本文章我们将了解在Java中检查null的必要性以及如何避免代码中的null
     

二.什么是NullPointerException

根据Javadoc for NullPointerException,当应用程序在需要对象的情况下尝试使用null时抛出它,例如:
  • 调用null对象的实例方法
  • 访问或修改对象的字段
  • null的长度视为数组
  • 访问或修改null的槽,就像它是一个数组一样
  • 抛出null就好像它是一个Throwable
让我们快速查看导致此异常的Java代码的几个示例:
public void doSomething() {
    String result = doSomethingElse();
    if (result.equalsIgnoreCase("Success")) 
        // success
    }
}
 
private String doSomethingElse() {
    return null;
}
在这里,我们尝试调用null引用的方法调用。这将导致NullPointerException。
另一个常见示例是,如果我们尝试访问空 数组:
public static void main(String[] args) {
    findMax(null);
}
 
private static void findMax(int[] arr) {
    int max = arr[0];
    
}
这会在第6行导致  NullPointerException
因此,访问空 对象的任何字段,方法或索引会导致  NullPointerException,如上面的示例所示。
避免NullPointerException的 常见方法  是检查null
public void doSomething() {
    String result = doSomethingElse();
    if (result != null && result.equalsIgnoreCase("Success")) {
        // success
    }
    else
        // failure
}
 
private String doSomethingElse() {
    return null;
}
在实际中,程序员发现很难识别哪些对象可以为  null。 积极安全的策略可能是为每个对象检查  null。但是,这会导致大量冗余空值 检查,并使我们的代码可读性降低。
在接下来的几节中,我们将介绍Java中的一些备选方案,以避免这种冗余。

三.通过API合理处理null

如上一节所述,访问null 对象的方法或变量会导致NullPointerException。 我们还讨论了在访问对象之前对对象进行空 检查可以消除NullPointerException的可能性
但是,通常有API可以处理  空 值。例如:
public void print(Object param) {
    System.out.println("Printing " + param);
}
 
public Object process() throws Exception {
    Object result = doSomething();
    if (result == null) {
        throw new Exception("Processing fail. Got a null response");
    } else {
        return result;
    }
}
在print()方法调用将只打印“空”,但不会抛出异常。同样,  process() 永远不会在其响应中返回  null 。它反而抛出异常。
因此,对于访问上述API的客户端代码,不需要进行  空 检查。
但是,此类API必须在合同中明确说明。API发布此类合同的常见位置是JavaDoc
但是,这并未明确指出API合同,因此依赖于客户端代码开发人员来确保其合规性。
在下一节中,我们将看到一些IDE和其他开发工具如何帮助开发人员解决这个问题。

四.自动化API检查

4.1。使用静态代码分析

静态代码分析工具有助于提高代码质量。一些这样的工具也允许开发人员维护契约。一个例子是FindBugs
FindBugs 通过@Nullable 和  @NonNull 注释帮助管理null 契约  。我们可以在任何方法,字段,局部变量或参数上使用这些注释。这使得对客户端代码明确指出注释类型是否为null 。我们来看一个例子:
public void accept(@Nonnull Object param) {
    System.out.println(param.toString());
}
在这里,  @ NonNull 清楚地表明参数不能为  null。 如果客户端代码在不检查null参数的情况下调用此方法  ,则 FindBugs将在编译时生成警告。 

4.2。使用IDE支持

开发人员通常依靠IDE来编写Java代码。智能代码完成和有用警告等功能,如可能没有分配变量,在很大程度上肯定有帮助。
一些IDE还允许开发人员管理API合同,从而消除对静态代码分析工具的需求。IntelliJ IDEA提供@NonNull 和  @Nullable 注释。要在IntelliJ中添加对这些注释的支持,我们必须添加以下Maven依赖项:
<dependency>
    <groupId>org.jetbrains</groupId>
    <artifactId>annotations</artifactId>
    <version>16.0.2</version>
</dependency>
现在,如果缺少空 检查IntelliJ将生成警告,就像我们在上一个示例中一样。
IntelliJ还提供了用于处理复杂API合同的提示

五.断言

到目前为止,我们只讨论过从客户端代码中删除空 检查的必要性。但是,这很少适用于实际应用。
现在,让我们假设我们正在使用一个不能接受空 参数的API,或者可以返回必须由客户端处理的  空 响应。这表明我们需要检查参数或值的响应。
在这里,我们可以使用Java Assertions而不是传统的null check条件语句
public void accept(Object param){
    assert param != null;
    doSomething(param);
}
在第2行中,我们检查null参数。如果启用了断言,则会导致  AssertionError。
尽管这是断言非参数等前置条件的好方法,但这种方法存在两个主要问题
  1. 通常在JVM中禁用断言
  2. 一个  虚假 的声明将导致在未经检查的错误是不可恢复
因此,建议程序员不要使用断言来检查条件。在以下部分中,我们将讨论处理null 验证的其他方法

六.通过编码实践避免检查

6.1。前提条件

编写早期失败的代码通常是一种很好的做法。因此,如果一个API接受不允许有多个参数为空这是更好地检查每一个非参数作为API的一个先决条件。
例如,让我们看看两个方法 - 一个早期失败,另一个不失败:
public void goodAccept(String one, String two, String three) {
    if (one == null || two == null || three == null) {
        throw new IllegalArgumentException();
    }
 
    process(one);
    process(two);
    process(three);
}
 
public void badAccept(String one, String two, String three) {
    if (one == null) {
        throw new IllegalArgumentException();
    } else {
        process(one);
    }
 
    if (two == null) {
        throw new IllegalArgumentException();
    } else {
        process(two);
    }
 
    if (three == null) {
        throw new IllegalArgumentException();
    } else {
        process(three);
    }
}
显然,我们应该更喜欢goodAccept()而不是badAccept()。
作为替代方案,我们也可以使用Guava的前置条件来验证API参数。

6.2。使用原语而不是包装类

由于  null 对于像int这样的原语来说不是一个可接受的值  , 我们应该尽可能优先于它们的包装对象,如Integer 
考虑一个对两个整数求和的方法的两个实现:
public static int primitiveSum(int a, int b) {
    return a + b;
}
 
public static Integer wrapperSum(Integer a, Integer b) {
    return a + b;
}
现在,让我们在客户端代码中调用这些API:
int sum = primitiveSum(null, 2);
这将导致编译时错误,因为  null 不是int的有效值  
当使用API​​与包装类时,我们得到一个NullPointerException
assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));
还有其他因素可以使用原语而不是包装器
 

6.3。空集合

有时,我们需要将一个集合作为方法的响应返回。对于这样的方法,我们应该总是尝试返回一个空集合而不是null
public List<String> names() {
    if (userExists()) {
        return Stream.of(readName()).collect(Collectors.toList());
    } else {
        return Collections.emptyList();
    }
}
因此,我们在调用此方法时避免了客户端执行检查的需要。

七.使用 Objects 

Java 7引入了新的Objects  API。此API有几个静态 实用程序方法,可以消除大量冗余代码。让我们看看一个这样的方法requireNonNull()
public void accept(Object param) {
    Objects.requireNonNull(param);
    // doSomething()
}
现在,让我们测试accept() 方法:
assertThrows(NullPointerException.class, () -> accept(null));
因此,如果将null 作为参数传递,则  accept()会抛出NullPointerException。
此类还具有  isNull() 和  nonNull() 方法,可用作谓词来检查对象是否为null。

八.使用Optional

Java 8 在该语言中引入了一个新的Optional API。与null相比,这为处理可选值提供了更好的合约。 让我们看看Optional 如何消除对空 检查的需要  :

public Optional<Object> process(boolean processed) {
    String response = doSomething(processed);
 
    if (response == null) {
        return Optional.empty();
    }
 
    return Optional.of(response);
}
 
private String doSomething(boolean processed) {
    if (processed) {
        return "passed";
    } else {
        return null;
    }
}
通过返回一个  可选选项, 如上图所示,该  处理()方法使得明确给调用者,响应可以是空的,并且必须在编译时被处理。
这显然消除了客户端代码中任何空 检查的需要  。可以使用Optional  API 的声明性样式以不同方式处理空响应:
assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));
此外,它还为API开发人员提供了一个更好的合同,以向客户表明API可以返回空响应。
虽然我们不需要  对此API的调用者进行空 检查,但我们使用它来返回空响应。为避免这种情况,  Optional提供了一个ofNullable 方法,该  方法返回一个具有指定值的Optional ,如果值为null,则返回  empty 
public Optional<Object> process(boolean processed) {
    String response = doSomething(processed);
    return Optional.ofNullable(response);
}

九.使用工具库

Lombok是一个很棒的库,可以减少项目中样板代码的数量。它附带了一组注释,取代了我们经常在Java应用程序中编写的代码的常见部分,例如getter,setter和toString(),仅举几例。
另一个注释是@NonNull。 因此,如果项目已经使用Lombok来消除样板代码,则@NonNull 可以替换空 检查的需要
在我们继续查看一些示例之前,让我们为Lombok 添加一个Maven依赖项:
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.6</version>
</dependency>
现在,我们可以在需要进行空 检查的地方  使用@NonNull 
public void accept(@NonNull Object param){
    System.out.println(param);
}
因此,我们只是注释了需要进行null 检查的对象,并且Lombok生成了已编译的类:
public void accept(@NonNull Object param) {
    if (param == null) {
        throw new NullPointerException("param");
    } else {
        System.out.println(param);
    }
}
如果  param null,则此方法抛出NullPointerException。 该方法必须在其合同中明确说明,并且客户端代码必须处理异常。

9.2。使用StringUtils

一般来说,字符串验证包括除空值检查值。因此,常见的验证声明是:
public void accept(String param){
    if (null != param && !param.isEmpty())
        System.out.println(param);
}
如果我们必须处理很多String 类型,这很快就会变得多余。这是  StringUtils 派上用场的地方。在我们看到这个动作之前,让我们为commons-lang3添加一个Maven依赖项:
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>
现在让我们用StringUtils重构上面的代码  
public void accept(String param) {
    if (StringUtils.isNotEmpty(param))
        System.out.println(param);
}
因此,我们使用静态 实用程序方法  isNotEmpty()替换了  null 或空检查  。 此API提供了其他强大的实用方法来处理常见的  String函数。