还在到处写 != null ?Java 代码异味该改改了

作者:微信公众号:【架构师老卢】
7-29 8:24
23

如果你已经用 Java 开发有段时间了,那你的代码库里很可能到处都是这个短句:!= null

if (user != null) {
    sendEmail(user);
}

是不是很眼熟?

乍一看没什么问题,但我想问问你:

  • 万一你漏掉了一个 != null 检查怎么办?
  • 如果你团队里的多个开发者对 != null 检查的方式不一致怎么办?
  • 这种方式会不会悄悄引入 bug ?

我写 Java 代码已经十多年了,有一个教训是我付出了很大代价才学到的:

❗️ 到处写 != null 根本不是什么策略 —— 这只是一种防御性的拼凑做法,往往会掩盖设计和沟通中更深层次的问题。

在本文中,我们将探讨:

  • 为什么到处写 != null 是代码异味
  • 空值检查在实际开发中的风险
  • 处理 Java 中“值不存在”的更聪明方式(附示例)
  • 什么时候使用 null 其实是合理的
  • 能让代码更易读、更可靠的最佳实践

为什么我们总在写 != null ?

原因很简单:Java 允许空引用,而我们都害怕那个可怕的 NullPointerException。所以我们就像买保险一样,到处加上 != null 检查。

if (order != null && order.getCustomer() != null) {
    System.out.println(order.getCustomer().getName());
}

这被称为防御性编程。虽然看起来很安全,但很容易失控。

到处写 != null 的真正问题

让我们来聊聊为什么这是一种不好的实践:

1. 它掩盖了糟糕的设计

如果你的对象在任何时候都可能为 null,问问自己:它们为什么会是 null ?

是数据缺失了?还是从未初始化过?或是中途被删除了?

示例:

User user = userService.getUserById(id);
if (user != null) {
    process(user);
}

我们没有去理解用户为什么可能为 null,而是跳过了这个更深层次的问题,只是简单地“防御”它。

2. 容易出错

你越依赖 != null,就越有可能在某个地方漏掉一次检查。你知道这意味着什么吗?

运行时会抛出 NullPointerException

更糟的是,这可能会在生产环境中特定条件下发生,而日志往往不会告诉你原因。

示例:

// 有人在这里漏掉了空值检查
order.getCustomer().getName(); // 崩溃

3. 让代码难以阅读

大量的空值检查会产生冗长、嵌套、难以理解的逻辑。

if (invoice != null) {
    if (invoice.getAmount() != null) {
        if (invoice.getAmount().compareTo(BigDecimal.ZERO) > 0) {
            processInvoice(invoice);
        }
    }
}

这样的代码可读性高吗?其实不然。

处理 Java 中 null 的更聪明方式

现在,让我们来看看那些比到处写 != null 更聪明、更简洁的替代方案。

1. 熟练使用 Optional

Java 8 引入了 Optional<T> —— 一个可能包含或不包含非 null 值的容器对象。

Optional<User> userOpt = userService.findUserById(id);
userOpt.ifPresent(this::processUser);

这迫使开发者以结构化的方式处理值的缺失。

为什么更好:

  • 让值的缺失变得明确,而非隐含
  • 避免意外的 null
  • 支持函数式编程

常见错误: 不要在不检查的情况下使用 Optional.get()

// 有风险
User user = userOpt.get(); // 如果为空,会抛出 NoSuchElementException

更安全的方式:

User user = userOpt.orElse(new GuestUser());

使用 map 和 flatMap 更优:

String name = userOpt.map(User::getName)
                     .orElse("Unknown");

2. 使用从不返回 null 的对象

设计你的方法,让它们从不返回 null。相反,返回:

  • 空集合(Collections.emptyList()
  • Optional 值(Optional.empty()
  • 特殊对象,如 GuestUserNullOrder

示例:

// ❌ 有风险 —— 方法可能返回 null,迭代时会导致 NPE
List<Product> products = productService.getAll(); // 不能保证安全

// ✅ 更好 —— 用 Optional 包装它以防止 null(如果不确定)
List<Product> products = Optional.ofNullable(productService.getAll())
                                 .orElse(Collections.emptyList());

// ✅ 最佳 —— 确保 getAll() 被实现为从不返回 null
// 内部实现:如果没有数据,返回 Collections.emptyList()
List<Product> products = productService.getAll(); // 保证安全

现在你的代码可以更简洁:

for (Product p : products) {
    display(p);
}

3. 采用空对象模式(Null Object Pattern)

有时候,与其返回 null,不如返回一个能安全运行的虚拟对象。

示例:

public class GuestUser extends User {
    public String getName() {
        return "Guest";
    }
    public boolean isAuthenticated() {
        return false;
    }
}

现在:

User user = userService.getOrGuest(id);
System.out.println(user.getName());

这避免了空值检查,让代码自然流畅。

4. 快速失败 —— 而非延迟失败

与其将 null 向下传递并在之后检查,不如在数据缺失时快速失败。

示例:

public void processUser(User user) {
    Objects.requireNonNull(user, "User must not be null");
    // 现在可以安全地使用 user 了
}

这样如果出现问题,会立即抛出清晰的错误信息,而不是在之后导致难以理解的故障。

5. 使用注解:@NonNull、@Nullable

使用 @NonNull@Nullable 等注解,并配置 Lombok、SpotBugs 或 IntelliJ 等工具,在编译时发出警告。

示例:

public void sendEmail(@NonNull User user) {
    // 如果有人传递 null,编译器会报错
}

这让你的代码自我文档化,并且工具会帮助你尽早发现错误。

实际重构:之前 vs 之后

让我们重构一段充满空值检查的典型代码。

之前:

if (order != null) {
    Customer c = order.getCustomer();
    if (c != null && c.getEmail() != null) {
        sendEmail(c.getEmail());
    }
}

之后(使用 Optional + 快速失败):

public void processOrder(Order order) {
    Objects.requireNonNull(order, "Order cannot be null");
    Optional.ofNullable(order.getCustomer())
            .map(Customer::getEmail)
            .ifPresent(this::sendEmail);
}

更简洁、更安全、更易于维护。

什么时候 null 其实是合理的

说实话 —— 有时候 null 是没问题的。

示例:

  • 框架回调(例如 Spring 的 @Nullable
  • 性能关键路径(避免对象创建)
  • 你无法控制的遗留 API

但即便如此,你也应该:

  • 限制 null 的使用范围
  • 清晰地文档化
  • 避免在公共 API 中暴露 null

给团队的更明智的 null 策略

与其盲目地写 != null,不如考虑以下决策树:

(此处将显示放大图像)

总结

null 本身并非洪水猛兽,但我们过度使用 != null 绝对是一个危险信号。

以下是编写 Java 代码的更明智方式:

  • 使用 Optional<T> 表示值的缺失
  • 使用空对象模式避免 null 值
  • 返回空集合而非 null
  • 使用 Objects.requireNonNull() 快速失败
  • 使用注解传达预期
  • 逐步重构现有代码

思考

我们无法完全消除 null —— 它们是 Java 基因的一部分。但我们可以控制它们。

通过摆脱重复的 != null 检查,我们开始编写更简洁、更安全、更具表达力的 Java 代码 —— 这些代码读起来像故事,运行起来也符合预期。

相关留言评论
昵称:
邮箱: