Java编程中如何避免NullPointerException

位置:首页>文章>详情   分类: 教程分享 > Java教程   阅读(1833)   2023-03-28 11:29:14

一.简述

  • 通常在引用和集合中处理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函数。
 
地址:https://www.leftso.com/article/563.html

相关阅读

Java编程中如何避免NullPointerException(空指针异常)
spring boot 开发技巧,在开发web项目中跳过thyemeleaf模板/js/css等缓存避免每次修改资源文件都需要重启服务器
前端状态数据展示小技巧
有一些REST技巧
Java如何复制目录,Java基础教程系列,如果要将目录及其包含的所有子文件夹和文件从一个位置复制到另一个位置,请使用下面的代码,该代码使用递归遍历目录结构,然后使用Files.copy()函数...
Spring Boot 2.0 - 开发者工具devtools热部署教程(自动重载),如果你曾经在最新的UI开发框架上工作过,比如Node,angular,gulp等等,那么当你在某些代码发生变...
JDK11 90+ 新特性新功能(第二部分:非开发人员功能)
Apache Shiro教程,您的第一个Apache Shiro应用程序(翻译)-编程技术
企业软件_企业软件定制开发框架选择以及分类
docker入门使用教程/linux(centos)系统docker制作教程