如果你已经用 Java 开发有段时间了,那你的代码库里很可能到处都是这个短句:!= null
。
if (user != null) {
sendEmail(user);
}
是不是很眼熟?
乍一看没什么问题,但我想问问你:
!= null
检查怎么办?!= null
检查的方式不一致怎么办?我写 Java 代码已经十多年了,有一个教训是我付出了很大代价才学到的:
❗️ 到处写 != null
根本不是什么策略 —— 这只是一种防御性的拼凑做法,往往会掩盖设计和沟通中更深层次的问题。
在本文中,我们将探讨:
!= null
是代码异味原因很简单:Java 允许空引用,而我们都害怕那个可怕的 NullPointerException
。所以我们就像买保险一样,到处加上 != null
检查。
if (order != null && order.getCustomer() != null) {
System.out.println(order.getCustomer().getName());
}
这被称为防御性编程。虽然看起来很安全,但很容易失控。
让我们来聊聊为什么这是一种不好的实践:
如果你的对象在任何时候都可能为 null,问问自己:它们为什么会是 null ?
是数据缺失了?还是从未初始化过?或是中途被删除了?
示例:
User user = userService.getUserById(id);
if (user != null) {
process(user);
}
我们没有去理解用户为什么可能为 null,而是跳过了这个更深层次的问题,只是简单地“防御”它。
你越依赖 != null
,就越有可能在某个地方漏掉一次检查。你知道这意味着什么吗?
运行时会抛出 NullPointerException
。
更糟的是,这可能会在生产环境中特定条件下发生,而日志往往不会告诉你原因。
示例:
// 有人在这里漏掉了空值检查
order.getCustomer().getName(); // 崩溃
大量的空值检查会产生冗长、嵌套、难以理解的逻辑。
if (invoice != null) {
if (invoice.getAmount() != null) {
if (invoice.getAmount().compareTo(BigDecimal.ZERO) > 0) {
processInvoice(invoice);
}
}
}
这样的代码可读性高吗?其实不然。
现在,让我们来看看那些比到处写 != null
更聪明、更简洁的替代方案。
Java 8 引入了 Optional<T>
—— 一个可能包含或不包含非 null 值的容器对象。
Optional<User> userOpt = userService.findUserById(id);
userOpt.ifPresent(this::processUser);
这迫使开发者以结构化的方式处理值的缺失。
为什么更好:
常见错误:
不要在不检查的情况下使用 Optional.get()
:
// 有风险
User user = userOpt.get(); // 如果为空,会抛出 NoSuchElementException
更安全的方式:
User user = userOpt.orElse(new GuestUser());
使用 map 和 flatMap 更优:
String name = userOpt.map(User::getName)
.orElse("Unknown");
设计你的方法,让它们从不返回 null。相反,返回:
Collections.emptyList()
)Optional.empty()
)GuestUser
、NullOrder
等示例:
// ❌ 有风险 —— 方法可能返回 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);
}
有时候,与其返回 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());
这避免了空值检查,让代码自然流畅。
与其将 null 向下传递并在之后检查,不如在数据缺失时快速失败。
示例:
public void processUser(User user) {
Objects.requireNonNull(user, "User must not be null");
// 现在可以安全地使用 user 了
}
这样如果出现问题,会立即抛出清晰的错误信息,而不是在之后导致难以理解的故障。
使用 @NonNull
、@Nullable
等注解,并配置 Lombok、SpotBugs 或 IntelliJ 等工具,在编译时发出警告。
示例:
public void sendEmail(@NonNull User user) {
// 如果有人传递 null,编译器会报错
}
这让你的代码自我文档化,并且工具会帮助你尽早发现错误。
让我们重构一段充满空值检查的典型代码。
之前:
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 是没问题的。
示例:
@Nullable
)但即便如此,你也应该:
与其盲目地写 != null
,不如考虑以下决策树:
(此处将显示放大图像)
null 本身并非洪水猛兽,但我们过度使用 != null
绝对是一个危险信号。
以下是编写 Java 代码的更明智方式:
Optional<T>
表示值的缺失Objects.requireNonNull()
快速失败我们无法完全消除 null —— 它们是 Java 基因的一部分。但我们可以控制它们。
通过摆脱重复的 != null
检查,我们开始编写更简洁、更安全、更具表达力的 Java 代码 —— 这些代码读起来像故事,运行起来也符合预期。