你真的处理好 null 了吗?——11种常见但容易被忽视的空值处理方式

引言

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 集合?

  1. 调用方容易忘记判空:大多数人习惯直接遍历集合,很少会去想集合本身可能是 null;
  2. 增加心智负担:每次调用都得想一下这个方法会不会返回 null;
  3. 代码冗余 :避免到处都是 if (list != null) 的判断;
  4. 违反直觉:集合的语义其实就是容器,空容器比 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。

其实最终还是看我们是否有意识地去应对它,并统一团队的写法习惯。

代码之道,始于判空,终于优雅。

相关推荐
越来越无动于衷1 小时前
基于 JWT 的登录验证功能实现详解
java·数据库·spring boot·mysql·mybatis
paopaokaka_luck4 小时前
基于SpringBoot+Uniapp的健身饮食小程序(协同过滤算法、地图组件)
前端·javascript·vue.js·spring boot·后端·小程序·uni-app
Villiam_AY4 小时前
Redis 缓存机制详解:原理、问题与最佳实践
开发语言·redis·后端
飛_5 小时前
解决VSCode无法加载Json架构问题
java·服务器·前端
木棉软糖7 小时前
一个MySQL的数据表最多能够存多少的数据?
java
魔尔助理顾问7 小时前
系统整理Python的循环语句和常用方法
开发语言·后端·python
程序视点8 小时前
Java BigDecimal详解:小数精确计算、使用方法与常见问题解决方案
java·后端
愿你天黑有灯下雨有伞8 小时前
Spring Boot SSE实战:SseEmitter实现多客户端事件广播与心跳保活
java·spring boot·spring
你的人类朋友8 小时前
❤️‍🔥微服务的拆分策略
后端·微服务·架构
Java初学者小白8 小时前
秋招Day20 - 微服务
java