JAVA

如何编写 Null-Safety 代码?

转载:如何编写 Null-Safety 代码?一文帮你讲透!

NullPointerExceptions(NPE)空指针异常应该是每个程序员的噩梦,作为 Java 程序员都知道:NPE 是运行时异常,在代码编译过程中很难发现这种异常。因此,这篇文章,我们将分析如何编写 null 安全代码。

在实际工作中,对于 null的处理通常有以下处理方式:

  • Nullable注解
  • NonNull注解
  • 显示处理 null
  • 使用 Optional类
  • 使用断言
  • 编写清晰的文档

Nullable注解


@Nullable是 Spring 的内置注解,可用于方法、参数或字段级别,用于标记方法的返回值或参数或字段可以为 null,通过使用 @Nullable  注解,开发者可以明确地表明某个方法可能返回 null 值,从而在调用该方法时更加谨慎。

如下示例:

// 示例1:用于方法
@Nullable
public String getUserById(Long userId) {
    if (userId == null) {
        return null;
    }
    // 根据userId查询用户邮箱
    return userRepository.findUserById(userId);
}


// 示例2:用于参数
User addUser(@NonNull Long userId, @Nullable String email);

// 示例3:用于字段
public class User {
  @NonNull
  Long userId;
  
  @Nullable
  String email;
}

在上述的示例中,@Nullable 用于方法,字段,参数,标识可以为 null,因此,方法的调用用就需要根据业务判断 null。

NonNull注解


@NonNull 是 Spring 的内置注解,可用于方法、参数或字段级别,用于标记方法的返回值或参数或字段不能为 null。通过使用 @NonNull 注解,开发者可以明确地表明某个方法不会返回 null 值,从而在调用该方法时可以放心地使用返回值。

如下示例:

import org.springframework.lang.NonNull;
// 示例1:用于方法
@NonNull
public User getUserById(Long userId) {
    // 根据userId查询用户信息
    User user = userRepository.findUserById(userId);
    if (user == null) {
        throw new IllegalArgumentException("User not found");
    }
    return user;
}

// 示例2:用于参数
User findUserById(@NonNull Long userId);

// 示例3:用于字段
public class User {
    @NonNull
    Long id;
}

在上述的示例中:示例1,@NonNull 注解标记方法其返回值不能为 null,因此,方法的调用者知道该方法不会返回 null 值,可以放心地使用返回值而不用担心空指针异常。

示例2,@NonNull 注解标记方法的入参不能为 null

示例3,@NonNull 注解标记字段不能为 null,一旦字段值为 null,IDE会出现以下警告:

显式处理 null


显式处理 null应该是我们使用最多也是最灵活处理 null的方法,在编写代码时,要注意对可能为 null 的值进行显式的空值检查和处理,可以使用 if 语句、try-catch块等方式来处理空值情况,避免程序崩溃或数据丢失。

如下代码,针对 user 对象是否为 null 做不同的处理:

// 方式1
if (user != null) {
    // 执行操作
} else {
    // 处理空值情况
}

// 方式2
try {
   // 业务处理
} catch (Exception e) {
  // 异常处理
}

使用 Optional类


Optional类是 Java 8 引入的,它的作用是封装一个可能为空的值,通过 Optional 类的方法,可以更加安全和优雅地处理空值情况,从而避免 NPE 空指针异常。

如下示例,针对上面手动处理 null, 我们可以使用 optional更优雅的实现 null 判断:

Optional<User> userOptional = Optional.ofNullable(user);
String userName = userOptional.map(User::getName).orElse("Unknown");

关于 Optional 更多的使用,参考下图:

使用 Optional 类的几个好处:

  • 可以避免 NPE 空指针异常,提高代码的健壮性;
  • 可以减少手动 null 判断,使代码更简洁和优雅;
  • 符合函数式编程的特性;

使用断言


断言 assert 可以帮助发现潜在的空指针异常问题,确保代码的假设得到正确验证。

如下示例:

assert user != null : "User object should not be null";

在上述示例中,我们使用断言来验证 user 对象不为 null,如果断言失败,则抛出相应的错误信息。

编写清晰的文档


在方法的注释中明确说明方法的返回值和参数是否可以为 null,以及如何处理 null情况。清晰的文档可以帮助其他开发者更好地理解代码逻辑和规范。

/**
 * 根据用户ID获取用户邮箱地址
 * @param userId 用户ID
 * @return 用户邮箱地址,可能为null
 */
@Nullable
public String getUserEmailById(Long userId) {
    // 方法实现
}

上述示例中,我们编写了清晰的方法注释,明确说明了方法的返回值可能为 null,帮助其他开发者更好地理解方法的使用规范和处理空值情况。

注意事项


对于上述 Spring 的 @Nullable@NonNull 注解,并非所有开发工具都能显示这些编译警告,如果未看到相关警告,请检查 IDE 中的编译器设置。这里以 IntelliJ 设置为例:

为了方便 IDE 进行一些自动化代码检查,我们可以使用 SpotBugs提供了一个 插件,可以检测由于可为 null 性而导致的代码异常。

安装上述插件后,如果任何用 @NonNull 注释的方法意外返回 null,则 SpotBugs 检查将失败,并出现类似于以下内容的错误:

[ERROR] High: xxx may return null, but is declared @Nonnull  At xxx.java:[line 36] NP_NONNULL_RETURN_VIOLATION

总结


本文,我们总结了对 null处理的几种通常方式:

  • Nullable注解
  • NonNull注解
  • 显示处理 null
  • 使用 Optional类
  • 使用断言
  • 编写清晰的文档

功夫在平常,功夫在细节!只要我们在日常开发中多注意一点细节,让好习惯成为自然,终有一天,不但可以写出让人羡慕的优雅代码,还可以写出高质量的代码。