如何避免在java中检查null语句(多种解决方案)

1. 概述

通常,null的变量、引用和集合在Java代码中很难处理。它们不仅很难辩别,而且处理起来也很复杂.

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

在本教程中,我们将了解在Java中检查处理null的必要性,以及帮助我们避免在代码中检查处理null的各种替代方法.

2. 什么是NullPointerException(NPE)?

根据Javadoc对于NullPointerException的定义,当应用程序在需要对象的地方对象为null时被抛出,例如:

1. 调用null对象的实例方法
2. 访问或修改null对象的字段
3. 当它是一个数组时取null的长度
4. 访问或修改null[]
5. 像抛出Throwable一样抛出null

    让我们快速看几个导致这种异常的Java代码示例:
示例1:

public void doSomething() {
    String result = doSomethingElse();
    if (result.equalsIgnoreCase("Success")) 
        // 处理逻辑
    }
}
 
private String doSomethingElse() {
    return null;
}

这里的result为null,在第3行中抛出NPE异常

示例2:

public void doSomething() {
    User user = getUser();
    if ("konastin".equalsIgnoreCase(user.getUsername())) 
        // 处理逻辑
    }
}
 
private User getUser() {
    return null;
}

第二行中的我们在获取user对象可能会获取null,第三行中抛出NPE异常

示例3:

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

这个例子第6行中抛出NPE异常

因此,访问null对象的任何字段、方法或索引都会导致NullPointerException,从上面的示例中看到这些.

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

public void doSomething() {
    User user = getUser();
    if (user!=null&&"konastin".equalsIgnoreCase(user.getUsername())) 
        // 处理逻辑
    }
}
 
private User getUser() {
    return null;
}

这里第三行进行null检查.

在真是开发中,程序员很难确定哪些对象为null.**一个非常安全的策略是为每个对象检查null.然而,这将导致大量冗余的null检查,并降低代码的可读性.**下面我们将通过Java中的一些替代方法来避免这种冗余.

3. 通过API规则来提示NullPointerException可能出现的情况

正如上一节所讨论的,访问null对象的方法或变量会导致NullPointerException。我们还讨论了在访问对象之前对其进行null检查可以消除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()方法调用只会打印“null”,但不会抛出异常。类似地,process()在其响应中永远不会返回null。它会抛出异常。

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

但是,这些api必须在它们的规则中明确表示。api发布此类规则的常见位置是JavaDoc。

然而,这并没有给出API规则的明确指示,因此依赖于客户机代码开发人员来确保其遵从。

4. 自动化API规则来提示NullPointerException可能出现的情况

4.1. 使用 Static Code Analysis

Static code analysis 工具可以极大地提高代码质量. 一些这样的工具还允许开发人员维护null规则。FindBugs就是一个例子。

FindBugs通过@Nullable和@NonNull注释帮助管理空规则。我们可以对任何方法、字段、局部变量或参数使用这些注释。这使得带注释的类型是否可以为空对客户机代码显式。我们来看一个例子:

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

这里@NonNull清楚地表明,参数不能为空。如果客户机代码调用此方法而没有检查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将生成一个警告,如果缺少null检查,就像我们上一个例子中那样。

IntelliJ还为处理复杂的API规则提供了一个规则注解。

5. 断言功能(Assertions)

到目前为止,我们只讨论了从客户机代码中删除null检查的必要性。但是,这在实际应用中很少适用。

现在,假设我们使用的API不能接受null参数,或者能够返回必须由客户机处理的null响应。这就需要检查参数或null值的响应。

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

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

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

虽然这是断言非空参数等先决条件的好方法,但是这种方法有两个主要问题:

JVM中通常禁用断言
错误断言会导致无法恢复的uncheck error

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

6. 通过代码练习如何进行Null检查

6.1. 提前处理

当我们开发程序时,可以通过提前进行NUll检查,当真实出现null时,这个检查会起作用,抛出NPE异常.
下面我会提供2个版本的代码,第一个goodAccept是较好的风格

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

另外,我们还可以使用Guava的先决条件来验证API参数。

6.2. 用包装类替换基本类型

由于int基本类型不能接收null,所以我们应该倾向于使用它的包装类Integer

如下思考两个函数:

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

我们在调用端调用这两个函数:

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

因此,我们避免了在调用此方法时的null检查。

7. 使用 Objects

Java 7引入的新的Objects API。这个API有几个静态实用程序方法,可以帮我们删除我们代码中大量冗余代码(检查null的代码)。让我们来看一个这样的方法requireNonNull():

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

下面测试accept() 方法:

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

因此,如果null作为参数传递,accept()将抛出NullPointerException。

该类还具有isNull()和nonNull()方法,可以将它们用作判断,来检查对象是否为null。

8. 使用Optional

Java 8在该语言中引入的一个新的可选API。与null相比,Optional为处理NPE提供了更好的规则。让我们看看可选如何消除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;
    }
}

通过返回一个Optional, process()方法向调用者表明,响应可以为空,并且必须在编译时处理。

这显著地消除了客户端代码中任何空检查的需要。空响应可以使用可选API的声明式以不同的方式处理:

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

此外,它还为API开发人员提供了更好的规则,以便向客户端表明API可以返回空响应。

虽然我们消除了对这个API调用者进行null检查的需要,但是我们使用它来返回一个空响应。为了避免这种情况, Optional 提供了一个ofNullable方法 ,这个方法会返回一个指定的值或者当出现null时返回empty:

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

9. 使用 Libraries

9.1. 使用Lombok

Lombok是一个很6的lib库,可以减少项目中的代码量。它附带了一组注解,这些注解代替了我们在Java应用程序中经常编写的常见代码部分,例如getter、setter和toString()等。

另外它的其中的一个注解 @NonNull.因此,如果一个项目已经使用Lombok,可以使用@NonNull代替null检查。

在我们继续看一些例子之前,让我们为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。方法必须在其按照@NonNull来显式地实现这一点,客户机代码必须处理异常。

9.2. 使用StringUtils

通常,字符串验证除了空值外,还包括对空值的检查。因此,一个通用的验证语句应该是:

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

如果我们必须处理许多字符串类型,这很快就会变得多余。这就是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或empty检查。这个API提供了其他强大的实用程序方法来处理公共字符串函数.

10. 总结

在本文中,我们研究了NullPointerException的各种原因,以及它难以识别的原因。然后,我们学习了在真实开发时,如何处理NPE,以及如何避免null检查,以此优化代码.java新手很值得学习.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章