引言
null,这个看似简单的词,成就了无数的 NullPointerException
。
在 Java 这个广阔的世界里,null 永远是个绕不开的存在和话题。
从最早的手动判空,到 Optional,再到各种注解、工具库......我们在写代码时几乎每天都要和它battle。
但你真的处理好 null 了吗?
今天我们不去长篇大论地讲空指针异常的历史(埋个坑,我们以后可以聊聊),也不做泛泛而谈。
只结合自己这些年写项目时的一点小心得,呈现几种最常见、但也最容易被忽视的空值处理方式。
不吹不黑,建议你耐心看完,一定会有所收获。

正文
基础操作:你都掌握了吗?
1. 传统判空:告别 if 地狱
反面教材 ❌:
java
if (user != null && user.getName() != null && user.getName().length() > 0) {
System.out.println("用户名:" + user.getName());
}
乍一看没问题,但:
- 链式访问容易漏判;
- 代码臃肿、难读;
- 多层嵌套,越写越像 if 地狱;
推荐写法 ✅:
java
// 方式1:卫语句 - 提前返回
if (user == null || user.getName() == null || user.getName().isEmpty()) {
System.out.println("用户名:匿名用户");
return;
}
System.out.println("用户名:" + user.getName());
// 方式2:三元运算符 - 简化逻辑
String displayName = (user != null && user.getName() != null && !user.getName().isEmpty())
? user.getName()
: "匿名用户";
System.out.println("用户名:" + displayName);
虽然代码会多几行,但是这样处理的好处很明显:
- 卫语句能减少嵌套层级,让主逻辑更清晰;
- 三元运算符适合简单的空值替换场景;
- 最终目的是使代码更易读,逻辑分离明确;
2. 工具类大合集:让判空逐渐优雅
Apache Commons、Hutool、SpringUtils... 各种第三方的判空工具,掌握这些让你事半功倍。
这里举点例子:
字符串判空:
java
// ❌ 原始写法
if (str != null && !str.trim().equals("")) { ... }
// ✅ Apache Commons
if (StringUtils.isNotBlank(str)) { ... }
// ✅ Hutool
if (StrUtil.isNotBlank(str)) { ... }
集合判空:
java
// ❌ 原始写法
if (list != null && !list.isEmpty()) { ... }
// ✅ Spring Utils
if (!CollectionUtils.isEmpty(list)) { ... }
// ✅ Apache Commons
if (CollectionUtils.isNotEmpty(list)) { ... }
对象判空:
java
// ❌ 原始写法
if (obj != null) { ... }
// ✅ Spring Utils
if (!ObjectUtils.isEmpty(obj)) { ... }
// ✅ Hutool
if (ObjUtil.isNotNull(obj)) { ... }
统一风格 + 提高可读性,选择一套坚持用下去就对了。
3. Optional 进阶:不仅判空,还能流式处理
相信 Optional 的基础用法大家都会,但是一些高级操作你都用过吗?
比如下面这些:
java
// 链式取值
String city = Optional.ofNullable(user)
.map(User::getProfile)
.map(Profile::getAddress)
.map(Address::getCity)
.orElse("未知城市");
// 条件过滤
Optional<User> activeUser = Optional.ofNullable(user)
.filter(u -> u.isActive())
.filter(u -> u.getAge() > 18);
// 存在即执行
Optional.ofNullable(user)
.ifPresent(u -> sendWelcomeEmail(u.getEmail()));
注意:
Optional
不推荐用作 方法参数;- 也不建议用于 类字段;
- 它可以用于返回值,用来明确告知调用方------该接口返回值可能缺失;
4. 参数校验:在入口拦截 null
最大的问题是,最开始把所有 null 都放进来了,后面只好到处加 if 去补救。
但成熟的代码应该在最外层就把 null 拦住,而不是留给内部逻辑去兜底。
比如我们接触最多的Spring Boot 项目:
java
@PostMapping("/create")
public void createUser(@Valid @RequestBody UserDTO user) {
// 这里的 user 已经通过校验,字段不会为 null
userService.save(user); // 无需再判空
}
// DTO 中的校验注解
public class UserDTO {
@NotNull(message = "用户名不能为空")
@NotBlank(message = "用户名不能为空字符串")
private String name;
@NotNull(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}
如果是普通方法调用:
java
public void createUser(UserDTO user) {
// 方法入口立即校验
Objects.requireNonNull(user, "用户信息不能为空");
Objects.requireNonNull(user.getName(), "用户名不能为空");
Objects.requireNonNull(user.getEmail(), "邮箱不能为空");
// 后续逻辑无需判空
userService.save(user);
}
核心思想:
- Web 层用
@Valid
注解校验; - Service 层用
Objects.requireNonNull()
校验; - 其他 Web 框架(如 Solon、Quarkus)也有类似的参数校验机制;
在入口处拦截,让内部逻辑专注于业务,减少心智负担,同时也避免业务代码更纯粹,不用被判空的逻辑污染。
5. 永远不要返回 null 集合
反面教材 ❌:
java
public List<Order> getOrders(String userId) {
if (noData) return null; // 猜猜后面会发生什么
return orderList;
}
// 调用方必须判空,否则...
List<Order> orders = getOrders("123");
for (Order order : orders) { // 触发 NullPointerException
System.out.println(order.getId());
}
为什么不建议返回 null 集合?
- 调用方容易忘记判空:大多数人习惯直接遍历集合,很少会去想集合本身可能是 null;
- 增加心智负担:每次调用都得想一下这个方法会不会返回 null;
- 代码冗余 :避免到处都是
if (list != null)
的判断; - 违反直觉:集合的语义其实就是容器,空容器比 null 更符合预期;
推荐写法 ✅:
java
public List<Order> getOrders(String userId) {
return Optional.ofNullable(orderList)
.orElse(Collections.emptyList());
}
// 调用方可以安心遍历
List<Order> orders = getOrders("123");
for (Order order : orders) { // 相当安全!即使没数据也不会报错
System.out.println(order.getId());
}
更好的做法:
- DAO 层、Service 层统一约定:永远不返回 null 集合或数组;
- 返回
Collections.emptyList()
或new ArrayList<>()
; - 调用方就能安心遍历、安心调用
size()
、isEmpty()
等方法;
看腻了?上干货:高效但少有人知
6. Objects.requireNonNull()
的隐藏用法
大多数人只知道它用来校验:
java
this.name = Objects.requireNonNull(name, "name cannot be null");
但它还有个神奇的返回值特性,配合IDE有神奇的功效:
java
// 一行代码既校验又赋值
public void setUser(User user) {
this.user = Objects.requireNonNull(user, "User cannot be null");
// user 在这里永远不会是 null,IDE 也能识别出来
}
// 在流式处理中当过滤器
users.stream()
.map(u -> Objects.requireNonNull(u.getEmail(), "Email missing"))
.collect(toList());
妙处就在于 IDE 的静态分析器会识别这个方法,后续代码会自动帮我们检查,不再有 空值警告。
7. 接口默认方法 + null 处理
Java 8+ 的接口默认方法可以做很多神奇的事:
java
interface UserService {
// 原始方法,可能返回 null
User findUserById(String id);
// 额外提供一个安全版本
default Optional<User> findUserSafely(String id) {
return Optional.ofNullable(findUserById(id));
}
// 额外提供另一个带默认值的版本
default User findUserOrDefault(String id) {
return Optional.ofNullable(findUserById(id))
.orElse(User.anonymous());
}
}
这样写, 调用方有三种选择,不仅三种方法能够见名知意,向后兼容还更安全,调用方只会夸我们 🐂🍺。
8. Record + 构造时校验
Java 14+ 的 Record 配合构造器校验:
java
public record User(String name, String email) {
// 紧凑构造器,自动校验
public User {
name = Objects.requireNonNull(name, "Name cannot be null");
email = Objects.requireNonNull(email, "Email cannot be null");
}
// 静态工厂方法处理可选字段
public static User createOptional(String name, String email) {
return new User(
Optional.ofNullable(name).orElse("Anonymous"),
Optional.ofNullable(email).orElse("unknown@example.com")
);
}
}
Record 天然不可变,配合构造时校验,直接从根源上消除 null 传播,把NPE扼杀在摇篮中。
9. @Nullable
/ @NonNull
注解 + IDE 静态检查
虽然是注解,但这是 编译时 的 null 处理,能在写代码时就发现潜在的空指针问题。
先看基础用法:
java
public class UserService {
// 明确声明可能返回 null
public @Nullable User findUser(String id) {
return repository.findById(id);
}
// 明确声明参数不能为 null
public void saveUser(@NonNull User user) {
repository.save(user);
}
// 字段也可以标注
@NonNull private String serviceName = "UserService";
@Nullable private User cachedUser;
}
IDE 智能提示在后续调用时就会有特殊的功效了:
java
// 1. 调用可能返回 null 的方法
User user = userService.findUser("123");
user.getName(); // IDE 黄色警告:Possible NPE!
// 2. 传入 null 参数
userService.saveUser(null); // IDE 警告:Parameter should not be null
// 3. 正确的处理方式
User user = userService.findUser("123");
if (user != null) {
user.getName(); // ✅ IDE 不再警告,智能推断非空
}
配置 IDE 检查级别:
- IntelliJ IDEA:Settings → Inspections → Probable bugs → Nullability problems
- 可以设置为 Error 级别,把警告升级为编译错误
- 支持整个项目的批量检查
这类的注解有好几种选择,一般主流注解有这些:
java
// 1. JetBrains 注解 (推荐,IDE 原生支持)
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
// 2. JSR-305 (Google、Spring 都在用)
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
// 3. Spring 框架注解
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
除了上面这两种注解,还有一个更加进阶的技巧,相信很多人都没有见过:
java
// 契约式编程:方法级别的空值约定
@Contract("null -> null; !null -> !null")
public @Nullable String processName(@Nullable String input) {
return input == null ? null : input.trim();
}
// IDE 能理解这个契约,智能推断返回值
String name = processName(user.getName());
// 如果 user.getName() 确定非空,IDE 就知道 name 也非空
结合这些注解,让我们能在写代码时就及时发现空指针的异常,非常高效。
10. Stream 的隐式 null 过滤
Stream 有个不太为人知的特性:
java
List<String> names = Arrays.asList("Alice", null, "Bob", null, "Charlie");
// 隐式过滤 null
List<String> validNames = names.stream()
.filter(Objects::nonNull) // 过滤 null
.map(String::toUpperCase)
.collect(toList());
// 更神奇的:flatMap 自动跳过 null Optional
List<Optional<String>> optionals = Arrays.asList(
Optional.of("Alice"),
Optional.empty(),
Optional.of("Bob")
);
List<String> result = optionals.stream()
.flatMap(Optional::stream) // 空的 Optional 被自动跳过
.collect(toList());
大招------你绝对想不到
说了这么多 Java 的 null 处理技巧,但说实话------你累不累?
我是挺累的。
看看 Kotlin 是怎么做的:
kotlin
// ❌ Java 写法:一堆判空逻辑
Optional.ofNullable(user)
.map(User::getProfile)
.map(Profile::getAddress)
.map(Address::getCity)
.orElse("Unknown");
// Kotlin 写法:安全调用操作符
val city = user?.profile?.address?.city ?: "Unknown"
一行代码解决所有问题!
更牛的是,Kotlin 在类型系统层面就区分了可空和非空:
kotlin
var name: String = "Alice" // 非空类型,编译器保证不会是 null
var email: String? = null // 可空类型,必须显式处理
name = null // ❌ 编译错误!
email?.length // ✅ 安全调用,null 时返回 null
email!!.length // ⚠️ 强制解包,确定非空时使用
类型系统直接告诉你哪里可能有 null,哪里绝对没有 null------这不比写一堆 Optional.ofNullable()
香吗?😏
而且,Kotlin 100% 兼容 Java,基本上可以渐进式迁移。
体验过 ?.
和 ?:
的丝滑后,谁还想再去写那又臭又长的裹脚布?
有时候,解决问题的最好方式,是解决掉提出问题的人。 🤣
写在最后
对于代码来说,null 本身不是错误,它只是一个状态的表达。
主要是我们对它缺乏约定、缺乏相应的有效的应对机制,所以才导致满世界的NPE。
其实最终还是看我们是否有意识地去应对它,并统一团队的写法习惯。
代码之道,始于判空,终于优雅。
