⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 苏三说技术 「苏三」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

一、传统判空的血泪史

某互联网金融平台因费用计算层级的空指针异常,导致凌晨产生9800笔错误交易。

DEBUG日志显示问题出现在如下代码段:

// 错误示例
BigDecimal amount = user.getWallet().getBalance().add(new BigDecimal("100"));

此类链式调用若中间环节出现null值,必定导致NPE。

初级阶段开发者通常写出多层嵌套式判断:

if(user != null){
Wallet wallet = user.getWallet();
if(wallet != null){
BigDecimal balance = wallet.getBalance();
if(balance != null){
// 实际业务逻辑
}
}
}

这种写法既不优雅又影响代码可读性。

那么,我们该如何优化呢?

二、Java 8+时代的判空革命

Java8之后,新增了Optional类,它是用来专门判空的。

能够帮你写出更加优雅的代码。

1. Optional黄金三板斧

// 重构后的链式调用
BigDecimal result = Optional.ofNullable(user)
.map(User::getWallet)
.map(Wallet::getBalance)
.map(balance -> balance.add(new BigDecimal("100")))
.orElse(BigDecimal.ZERO);

高级用法:条件过滤

Optional.ofNullable(user)
.filter(u -> u.getVipLevel() > 3)
.ifPresent(u -> sendCoupon(u)); // VIP用户发券

2. Optional抛出业务异常

BigDecimal balance = Optional.ofNullable(user)
.map(User::getWallet)
.map(Wallet::getBalance)
.orElseThrow(() -> new BusinessException("用户钱包数据异常"));

3. 封装通用工具类

public class NullSafe {

// 安全获取对象属性
publicstatic <T, R> R get(T target, Function<T, R> mapper, R defaultValue) {
return target != null ? mapper.apply(target) : defaultValue;
}

// 链式安全操作
publicstatic <T> T execute(T root, Consumer<T> consumer) {
if (root != null) {
consumer.accept(root);
}
return root;
}
}

// 使用示例
NullSafe.execute(user, u -> {
u.getWallet().charge(new BigDecimal("50"));
logger.info("用户{}已充值", u.getId());
});

三、现代化框架的判空银弹

4. Spring实战技巧

Spring中自带了一些好用的工具类,比如:CollectionUtils、StringUtils等,可以非常有效的进行判空。

具体代码如下:

// 集合判空工具
List<Order> orders = getPendingOrders();
if (CollectionUtils.isEmpty(orders)) {
return Result.error("无待处理订单");
}

// 字符串检查
String input = request.getParam("token");
if (StringUtils.hasText(input)) {
validateToken(input);
}

5. Lombok保驾护航

我们在日常开发中的entity对象,一般会使用Lombok框架中的注解,来实现getter/setter方法。

其实,这个框架中也提供了@NonNull等判空的注解。

比如:

@Getter
@Setter
public class User {
@NonNull // 编译时生成null检查代码
private String name;

private Wallet wallet;
}

// 使用构造时自动判空
User user = new User(@NonNull "张三", wallet);

四、工程级解决方案

6. 空对象模式

public interface Notification {
void send(String message);
}

// 真实实现
publicclass EmailNotification implements Notification {
@Override
public void send(String message) {
// 发送邮件逻辑
}
}

// 空对象实现
publicclass NullNotification implements Notification {
@Override
public void send(String message) {
// 默认处理
}
}

// 使用示例
Notification notifier = getNotifier();
notifier.send("系统提醒"); // 无需判空

7. Guava的Optional增强

其实Guava工具包中,给我们提供了Optional增强的功能。

比如:

import com.google.common.base.Optional;

// 创建携带缺省值的Optional
Optional<User> userOpt = Optional.fromNullable(user).or(defaultUser);

// 链式操作配合Function
Optional<BigDecimal> amount = userOpt.transform(u -> u.getWallet())
.transform(w -> w.getBalance());

Guava工具包中的Optional类已经封装好了,我们可以直接使用。

五、防御式编程进阶

8. Assert断言式拦截

其实有些Assert断言类中,已经做好了判空的工作,参数为空则会抛出异常。

这样我们就可以直接调用这个断言类。

例如下面的ValidateUtils类中的requireNonNull方法,由于它内容已经判空了,因此,在其他地方调用requireNonNull方法时,如果为空,则会直接抛异常。

我们在业务代码中,直接调用requireNonNull即可,不用写额外的判空逻辑。

例如:

public class ValidateUtils {
publicstatic <T> T requireNonNull(T obj, String message) {
if (obj == null) {
thrownew ServiceException(message);
}
return obj;
}
}

// 使用姿势
User currentUser = ValidateUtils.requireNonNull(
userDao.findById(userId),
"用户不存在-ID:" + userId
);

9. 全局AOP拦截

我们在一些特殊的业务场景种,可以通过自定义注解 + 全局AOP拦截器的方式,来实现实体或者字段的判空。

例如:

@Aspect
@Component
publicclass NullCheckAspect {

@Around("@annotation(com.xxx.NullCheck)")
public Object checkNull(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg == null) {
thrownew IllegalArgumentException("参数不可为空");
}
}
return joinPoint.proceed();
}
}

// 注解使用
public void updateUser(@NullCheck User user) {
// 方法实现
}

六、实战场景对比分析

场景1:深层次对象取值

// 旧代码(4层嵌套判断)
if (order != null) {
User user = order.getUser();
if (user != null) {
Address address = user.getAddress();
if (address != null) {
String city = address.getCity();
// 使用city
}
}
}

// 重构后(流畅链式)
String city = Optional.ofNullable(order)
.map(Order::getUser)
.map(User::getAddress)
.map(Address::getCity)
.orElse("未知城市");

场景2:批量数据处理

List<User> users = userService.listUsers();

// 传统写法(显式迭代判断)
List<String> names = new ArrayList<>();
for (User user : users) {
if (user != null && user.getName() != null) {
names.add(user.getName());
}
}

// Stream优化版
List<String> nameList = users.stream()
.filter(Objects::nonNull)
.map(User::getName)
.filter(Objects::nonNull)
.collect(Collectors.toList());

七、性能与安全的平衡艺术

上面介绍的这些方案都可以使用,但除了代码的可读性之外,我们还需要考虑一下性能因素。

下面列出了上面的几种在CPU消耗、内存只用和代码可读性的对比:

方案 CPU消耗 内存占用 代码可读性 适用场景
多层if嵌套 低 低 ★☆☆☆☆ 简单层级调用
Java Optional 中 中 ★★★★☆ 中等复杂度业务流
空对象模式 高 高 ★★★★★ 高频调用的基础服务
AOP全局拦截 中 低 ★★★☆☆ 接口参数非空验证

黄金法则

  • Web层入口强制参数校验
  • Service层使用Optional链式处理
  • 核心领域模型采用空对象模式

八、扩展技术

除了,上面介绍的常规判空之外,下面再给大家介绍两种扩展的技术。

Kotlin的空安全设计

虽然Java开发者无法直接使用,但可借鉴其设计哲学:

val city = order?.user?.address?.city ?: "default"

JDK 14新特性预览

// 模式匹配语法尝鲜
if (user instanceof User u && u.getName() != null) {
System.out.println(u.getName().toUpperCase());
}

总之,优雅判空不仅是代码之美,更是生产安全底线。

本文分享了代码判空的10种方案,希望能够帮助你编写出既优雅又健壮的Java代码。

文章目录
  1. 1. 一、传统判空的血泪史
  2. 2. 二、Java 8+时代的判空革命
    1. 2.1. 1. Optional黄金三板斧
    2. 2.2. 2. Optional抛出业务异常
    3. 2.3. 3. 封装通用工具类
  3. 3. 三、现代化框架的判空银弹
    1. 3.1. 4. Spring实战技巧
    2. 3.2. 5. Lombok保驾护航
  4. 4. 四、工程级解决方案
    1. 4.1. 6. 空对象模式
    2. 4.2. 7. Guava的Optional增强
  5. 5. 五、防御式编程进阶
    1. 5.1. 8. Assert断言式拦截
    2. 5.2. 9. 全局AOP拦截
  6. 6. 六、实战场景对比分析
    1. 6.1. 场景1:深层次对象取值
    2. 6.2. 场景2:批量数据处理
  7. 7. 七、性能与安全的平衡艺术
  8. 8. 八、扩展技术
    1. 8.1. Kotlin的空安全设计
    2. 8.2. JDK 14新特性预览