Avoid Check for Null Statement in Java – 避免在Java中检查空语句

最后修改: 2019年 4月 8日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Generally, null variables, references and collections are tricky to handle in Java code. They are not only hard to identify but also complex to deal with.

一般来说,null变量、引用和集合在Java代码中处理起来很棘手。它们不仅难以识别,而且处理起来也很复杂。

As a matter of fact, any miss in dealing with null cannot be identified at compile time and results in a NullPointerException at runtime.

事实上,在处理null时的任何失误都无法在编译时识别,并在运行时导致NullPointerException

In this tutorial, we’ll take a look at the need to check for null in Java and various alternatives that help us to avoid null checks in our code.

在本教程中,我们将看看在Java中检查null的必要性,以及帮助我们在代码中避免null检查的各种选择。

2. What Is NullPointerException?

2.什么是NullPointerException

According to the Javadoc for NullPointerException, it’s thrown when an application attempts to use null in a case where an object is required, such as:

根据Javadoc for NullPointerException,当应用程序试图在需要对象的情况下使用null时,它就会被抛出,例如。

  • Calling an instance method of a null object
  • Accessing or modifying a field of a null object
  • Taking the length of null as if it were an array
  • Accessing or modifying the slots of null as if it were an array
  • Throwing null as if it were a Throwable value

Let’s quickly see a few examples of the Java code that cause this exception:

让我们快速看一下导致这种异常的Java代码的几个例子。

public void doSomething() {
    String result = doSomethingElse();
    if (result.equalsIgnoreCase("Success")) 
        // success
    }
}

private String doSomethingElse() {
    return null;
}

Here, we tried to invoke a method call for a null reference. This would result in a NullPointerException.

这里,我们试图为一个空的引用调用一个方法。这将导致一个NullPointerException.

Another common example is if we try to access a null array:

另一个常见的例子是,如果我们试图访问一个null数组。

public static void main(String[] args) {
    findMax(null);
}

private static void findMax(int[] arr) {
    int max = arr[0];
    //check other elements in loop
}

This causes a NullPointerException at line 6.

这导致第6行出现NullPointerException

So, accessing any field, method, or index of a null object causes a NullPointerException, as can be seen from the examples above.

所以,访问一个null对象的任何字段、方法或索引都会导致一个NullPointerException,从上面的例子可以看出。

A common way of avoiding the NullPointerException is to check for null:

避免NullPointerException的一个常见方法是检查null

public void doSomething() {
    String result = doSomethingElse();
    if (result != null && result.equalsIgnoreCase("Success")) {
        // success
    }
    else
        // failure
}

private String doSomethingElse() {
    return null;
}

In the real world, programmers find it hard to identify which objects can be null. An aggressively safe strategy could be to check null for every object. However, this causes a lot of redundant null checks and makes our code less readable.

在现实世界中,程序员发现很难确定哪些对象可能是null。一个积极安全的策略可能是为每个对象检查null。然而,这将导致大量冗余的null检查,并使我们的代码变得不那么可读。

In the next few sections, we’ll go through some of the alternatives in Java that avoid such redundancy.

在接下来的几节中,我们将通过Java中的一些替代方案来避免这种冗余。

3. Handling null Through the API Contract

3.通过API合同处理null

As discussed in the last section, accessing methods or variables of null objects causes a NullPointerException. We also discussed that putting a null check on an object before accessing it eliminates the possibility of NullPointerException.

正如上一节所讨论的,访问null对象的方法或变量会导致NullPointerException。我们还讨论过,在访问对象之前对其进行null检查,可以消除NullPointerException的可能性。

However, there are often APIs that can handle null values:

然而,通常有一些API可以处理null值。

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;
    }
}

The print() method call would just print “null” but won’t throw an exception. Similarly, process() would never return null in its response. It rather throws an Exception.

print()方法调用将只是打印 “null”,但不会抛出一个异常。同样地,process()绝不会在其响应中返回null。而是抛出一个Exception

So, for a client code accessing the above APIs, there is no need for a null check.

因此,对于访问上述API的客户端代码,不需要进行null检查。

However, such APIs need to make it explicit in their contract. A common place for APIs to publish such a contract is the Javadoc.

然而,此类API需要在其合同中明确说明。Javadoc是API发布此类合同的一个常见场所。

But this gives no clear indication of the API contract and thus relies on the client code developers to ensure its compliance.

但这并没有给出API合同的明确指示,因此要依靠客户端代码开发人员来确保其合规性。

In the next section, we’ll see how a few IDEs and other development tools help developers with this.

在下一节中,我们将看到一些IDE和其他开发工具是如何帮助开发者解决这个问题的。

4. Automating API Contracts

4.自动化API合同

4.1. Using Static Code Analysis

4.1.使用静态代码分析

Static code analysis tools help improve the code quality a great deal. And a few such tools also allow the developers to maintain the null contract. One example is FindBugs.

静态代码分析工具有助于极大地提高代码质量。而且有一些这样的工具还允许开发人员维护null契约。一个例子是FindBugs

FindBugs helps manage the null contract through the @Nullable and @NonNull annotations. We can use these annotations over any method, field, local variable, or parameter. This makes it explicit to the client code whether the annotated type can be null or not.

FindBugs通过@Nullable @NonNull 注解帮助管理null 契约。我们可以在任何方法、字段、局部变量或参数上使用这些注解。这使得客户端代码明确了解被注解的类型是否可以是null或不是。

Let’s see an example:

让我们看一个例子。

public void accept(@NonNull Object param) {
    System.out.println(param.toString());
}

Here, @NonNull makes it clear that the argument cannot be null. If the client code calls this method without checking the argument for null, FindBugs would generate a warning at compile time.

这里,@NonNull明确指出参数不能是null如果客户端代码在调用这个方法时没有检查参数是否为null,FindBugs将在编译时产生一个警告。

4.2. Using IDE Support

4.2.使用IDE支持

Developers generally rely on IDEs for writing Java code. And features such as smart code completion and useful warnings, for example when a variable may not have been assigned, certainly help a lot.

开发人员通常依赖IDE来编写Java代码。而诸如智能代码完成和有用的警告等功能,例如当一个变量可能没有被分配时,当然有很大的帮助。

Some IDEs also allow developers to manage API contracts and thereby eliminate the need for a static code analysis tool. IntelliJ IDEA provides the @NonNull and @Nullable annotations.

一些IDE还允许开发人员管理API合约,从而消除对静态代码分析工具的需求。IntelliJ IDEA提供了@NonNull @Nullable 注解。

To add the support for these annotations in IntelliJ, we need to add the following Maven dependency:

为了在IntelliJ中添加对这些注释的支持,我们需要添加以下Maven依赖。

<dependency>
    <groupId>org.jetbrains</groupId>
    <artifactId>annotations</artifactId>
    <version>16.0.2</version>
</dependency>

Now IntelliJ will generate a warning if the null check is missing, as in our last example.

现在如果缺少null检查,IntelliJ将产生一个警告,就像我们上一个例子一样。

IntelliJ also provides a Contract annotation for handling complex API contracts.

IntelliJ还提供了一个Contract注解,用于处理复杂的API合同。

5. Assertions

5.断言

Until now, we’ve only talked about removing the need for null checks from the client code. But that is rarely applicable in real-world applications.

到目前为止,我们只谈到了从客户端代码中移除对null检查的需求。但这在现实世界的应用中很少适用。

Now let’s suppose that we’re working with an API that cannot accept null parameters or can return a null response that has to be handled by the client. This presents the need for us to check the parameters or the response for a null value.

现在让我们假设我们正在使用的API不能接受null参数,或者可以返回必须由客户端处理的null响应。这就需要我们检查参数或响应是否为null值。

Here, we can use Java Assertions instead of the traditional null check conditional statement:

在这里,我们可以使用Java断言来代替传统的null检查条件语句。

public void accept(Object param){
    assert param != null;
    doSomething(param);
}

In line 2, we check for a null parameter. If the assertions are enabled, this would result in an AssertionError.

在第2行,我们检查一个null参数。如果断言被启用,这将导致一个AssertionError

Although it is a good way of asserting preconditions such as non-null parameters, this approach has two major problems:

虽然这是一种断言前提条件的好方法,例如非参数,这种方法有两个主要问题

  1. Assertions are usually disabled in a JVM.
  2. A false assertion results in an unchecked error that is irrecoverable.

Therefore, it is not recommended for programmers to use Assertions for checking conditions. In the following sections, we’ll discuss other ways of handling null validations.

因此,不建议程序员使用断言来检查条件。在接下来的章节中,我们将讨论处理null验证的其他方法。

6. Avoiding Null Checks Through Coding Practices

6.通过编码实践避免Null检查

6.1. Preconditions

6.1.前提条件

It’s usually a good practice to write code that fails early. So, if an API accepts multiple parameters that aren’t allowed to be null, it’s better to check for every non-null parameter as a precondition of the API.

通常情况下,编写早期失败的代码是一种好的做法。因此,如果一个API接受多个不允许为null的参数,最好将检查每个非null参数作为API的前提条件。

Let’s look at two methods — one that fails early and one that doesn’t:

让我们看看两种方法–一种是早期失败,一种是不失败。

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);
    }
}

Clearly, we should prefer goodAccept() over badAccept().

显然,我们应该选择goodAccept()而不是badAccept()

As an alternative, we can also use Guava’s Preconditions for validating API parameters.

作为一种选择,我们也可以使用Guava的前提条件来验证API参数。

6.2. Using Primitives Instead of Wrapper Classes

6.2.使用基元而不是封装类

Since null is not an acceptable value for primitives like int, we should prefer them over their wrapper counterparts like Integer wherever possible.

由于null不是int等基元的可接受的值,我们应该尽可能地选择它们而不是Integer等对应的封装器。

Consider two implementations of a method that sums two integers:

考虑一个对两个整数求和的方法的两种实现。

public static int primitiveSum(int a, int b) {
    return a + b;
}

public static Integer wrapperSum(Integer a, Integer b) {
    return a + b;
}

Now let’s call these APIs in our client code:

现在让我们在我们的客户代码中调用这些API。

int sum = primitiveSum(null, 2);

This would result in a compile-time error since null is not a valid value for an int.

这将导致一个编译时错误,因为null不是int的有效值。

And when using the API with wrapper classes, we get a NullPointerException:

而在使用带有包装类的API时,我们会得到一个NullPointerException

assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));

There are also other factors for using primitives over wrappers, as we covered in another tutorial, Java Primitives Versus Objects.

正如我们在另一篇教程Java Primitives Versus Objects中所述,使用基元而不是封装器还有其他因素。

6.3. Empty Collections

6.3.空的集合

Occasionally, we need to return a collection as a response from a method. For such methods, we should always try to return an empty collection instead of null:

偶尔,我们需要从一个方法中返回一个集合作为响应。对于这样的方法,我们应该总是尝试返回一个空的集合而不是null

public List<String> names() {
    if (userExists()) {
        return Stream.of(readName()).collect(Collectors.toList());
    } else {
        return Collections.emptyList();
    }
}

This way, we’ve avoided the need for our client to perform a null check when calling this method.

这样,我们就避免了客户在调用这个方法时进行null检查。

7. Using Objects

7.使用对象

Java 7 introduced the new Objects API. This API has several static utility methods that take away a lot of redundant code.

Java 7引入了新的Objects API。这个API有几个静态实用方法,可以带走很多多余的代码。

Let’s look at one such method, requireNonNull():

让我们看一下这样一个方法,requireNonNull()

public void accept(Object param) {
    Objects.requireNonNull(param);
    // doSomething()
}

Now let’s test the accept() method:

现在我们来测试一下accept() 方法。

assertThrows(NullPointerException.class, () -> accept(null));

So, if null is passed as an argument, accept() throws a NullPointerException.

因此,如果null被作为一个参数传递,accept()会抛出一个NullPointerException

This class also has isNull() and nonNull() methods that can be used as predicates to check an object for null.

这个类也有isNull()nonNull()方法,可以作为谓词来检查一个对象的null

8. Using Optional

8.使用可选的

8.1. Using orElseThrow

8.1.使用orElseThrow

Java 8 introduced a new Optional API in the language. This offers a better contract for handling optional values compared to null.

Java 8 在语言中引入了一个新的 OptionalAPI。与null相比,它为处理可选值提供了更好的契约。

Let’s see how Optional takes away the need for null checks:

让我们看看Optional是如何消除对null检查的需要。

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;
    }
}

By returning an Optional, as shown above, the process method makes it clear to the caller that the response can be empty and needs to be handled at compile time.

通过返回一个Optional,如上所示,process方法向调用者表明,响应可能是空的,需要在编译时处理。

This notably takes away the need for any null checks in the client code. An empty response can be handled differently using the declarative style of the Optional API:

这明显地消除了在客户端代码中对任何null检查的需要。可以使用OptionalAPI的声明式风格对空响应进行不同的处理。

assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));

Furthermore, it also provides a better contract to API developers to signify to the clients that an API can return an empty response.

此外,它还为API开发者提供了更好的契约,以向客户表明API可以返回空响应。

Although we eliminated the need for a null check on the caller of this API, we used it to return an empty response.

尽管我们消除了对这个API的调用者进行null 检查的需要,但我们用它来返回一个空的响应。

To avoid this, Optional provides an ofNullable method that returns an Optional with the specified value, or empty, if the value is null:

为了避免这种情况,Optional提供了一个ofNullable方法,该方法返回一个具有指定值的Optional,如果值为null,则返回empty

public Optional<Object> process(boolean processed) {
    String response = doSomething(processed);
    return Optional.ofNullable(response);
}

8.2. Using Optional With Collections

8.2.在集合中使用Optional

While dealing with empty collections, Optional comes in handy:

在处理空的集合时,Optional会派上用场。

public String findFirst() {
    return getList().stream()
      .findFirst()
      .orElse(DEFAULT_VALUE);
}

This function is supposed to return the first item of a list. The Stream API’s findFirst function will return an empty Optional when there is no data. Here, we have used orElse to provide a default value instead.

这个函数应该是返回一个列表的第一个项目。Stream API的findFirst函数将在没有数据时返回一个空的Optional。在这里,我们使用orElse来提供一个默认值来代替。

This allows us to handle either empty lists or lists that, after we have used the Stream library’s filter method, have no items to supply.

这允许我们处理空的列表或者在我们使用Stream库的filter方法后,没有项目可以提供的列表。

Alternatively, we can also allow the client to decide how to handle empty by returning Optional from this method:

另外,我们也可以通过从这个方法返回Optional,让客户端决定如何处理empty

public Optional<String> findOptionalFirst() {
    return getList().stream()
      .findFirst();
}

Therefore, if the result of getList is empty, this method will return an empty Optional to the client.

因此,如果getList的结果是空的,这个方法将返回一个空的Optional给客户端。

Using Optional with collections allows us to design APIs that are sure to return non-null values, thus avoiding explicit null checks on the client.

对集合使用Optional允许我们设计肯定会返回非空值的API,从而避免在客户端进行明确的null检查。

It’s important to note here that this implementation relies on getList not returning null. However, as we discussed in the last section, it’s often better to return an empty list rather than a null.

这里需要注意的是,这个实现依赖于getList不返回null。然而,正如我们在上一节所讨论的,返回一个空的列表往往比返回一个null更好。

8.3. Combining Optionals

8.3.合并期权

When we start making our functions return Optional, we need a way to combine their results into a single value.

当我们开始让我们的函数返回Optional时,我们需要一种方法来把它们的结果合并成一个单一的值。

Let’s take our getList example from earlier. What if it were to return an Optional list, or were to be wrapped with a method that wrapped a null with Optional using ofNullable?

让我们来看看前面的 getList 例子。如果它返回一个 Optional 列表,或者被一个使用 ofNullablenullOptional 包装在一起的方法包裹起来,会怎么样?

Our findFirst method wants to return an Optional first element of an Optional list:

我们的findFirst方法想要返回一个Optional列表的第一个元素Optional

public Optional<String> optionalListFirst() {
   return getOptionalList()
      .flatMap(list -> list.stream().findFirst());
}

By using the flatMap function on the Optional returned from getOptional, we can unpack the result of an inner expression that returns Optional. Without flatMap, the result would be Optional<Optional<String>>. The flatMap operation is only performed when the Optional is not empty.

通过对从getOptional返回的Optional使用flatMap函数,我们可以对返回Optional的内部表达式的结果进行解包。如果没有flatMap,结果将是Optional<Optional<String>>/em>。flatMap操作只有在Optional不为空时才会执行。

9. Libraries

9.图书馆

9.1. Using Lombok

9.1.使用Lombok

Lombok is a great library that reduces the amount of boilerplate code in our projects. It comes with a set of annotations that take the place of common parts of code we often write ourselves in Java applications, such as getters, setters and toString(), to name a few.

Lombok是一个伟大的库,可以减少我们项目中的模板代码量。它带有一组注释,可以取代我们在Java应用程序中经常自己编写的代码的常见部分,例如getters、setters和toString(),仅举几例。

Another of its annotations is @NonNull. So, if a project already uses Lombok to eliminate boilerplate code, @NonNull can replace the need for null checks.

它的另一个注解是@NonNull。因此,如果一个项目已经使用Lombok来消除模板代码,@NonNull可以取代对null检查的需求。

Before we move on to some examples, let’s add a Maven dependency for Lombok:

在我们继续讨论一些例子之前,让我们为Lombok添加一个Maven依赖项。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>

Now we can use @NonNull wherever a null check is needed:

现在我们可以在需要检查@NonNull的地方使用null

public void accept(@NonNull Object param){
    System.out.println(param);
}

So, we simply annotated the object for which the null check would’ve been required, and Lombok generates the compiled class:

因此,我们简单地注释了需要进行null 检查的对象,然后Lombok生成了编译后的类。

public void accept(@NonNull Object param) {
    if (param == null) {
        throw new NullPointerException("param");
    } else {
        System.out.println(param);
    }
}

If param is null, this method throws a NullPointerException. The method must make this explicit in its contract, and the client code must handle the exception.

如果paramnull,这个方法会抛出一个NullPointerException该方法必须在其契约中明确说明这一点,并且客户端代码必须处理该异常。

9.2. Using StringUtils

9.2.使用StringUtils

Generally, String validation includes a check for an empty value in addition to null value.

一般来说,String验证除了null值之外,还包括对空值的检查。

Therefore, this would be a common validation statement:

因此,这将是一个常见的验证性声明。

public void accept(String param){
    if (null != param && !param.isEmpty())
        System.out.println(param);
}

This quickly becomes redundant if we have to deal with a lot of String types. This is where StringUtils comes in handy.

如果我们必须处理大量的String类型,这很快就变得多余了。这就是StringUtils的用武之地。

Before we see this in action, let’s add a Maven dependency for commons-lang3:

在实际操作之前,我们先为commons-lang3添加一个Maven依赖项。

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

Let’s now refactor the above code with StringUtils:

现在让我们用StringUtils重构上面的代码。

public void accept(String param) {
    if (StringUtils.isNotEmpty(param))
        System.out.println(param);
}

So, we replaced our null or empty check with a static utility method isNotEmpty(). This API offers other powerful utility methods for handling common String functions.

因此,我们用一个静态实用方法isNotEmpty()取代了我们的null或空检查。这个API提供了其他强大的实用方法来处理常见的String函数。

10. Conclusion

10.结语

In this article, we looked at the various reasons for NullPointerException and why it is hard to identify.

在这篇文章中,我们研究了NullPointerException的各种原因,以及为什么它很难识别。

Then we saw various ways to avoid the redundancy in code around checking for null with parameters, return types and other variables.

然后,我们看到了各种方法来避免代码中围绕检查参数、返回类型和其他变量的null的冗余。

All the examples are available over on GitHub.

所有的例子都可以在GitHub上找到over