[其他] 瞧瞧别人家的判空,那叫一个优雅!

[复制链接]
 楼主| 科叼 发表于 2025-4-3 13:57 | 显示全部楼层 |阅读模式
后端, se, TI, US, UL, ge
原文:https://juejin.cn/post/7478221220074504233
一、传统判空的血泪史
某互联网金融平台因费用计算层级的空指针异常,导致凌晨产生9800笔错误交易。
DEBUG日志显示问题出现在如下代码段:
  1. // 错误示例
  2. BigDecimal amount = user.getWallet().getBalance().add(new BigDecimal("100"));

此类链式调用若中间环节出现null值,必定导致NPE。
初级阶段开发者通常写出多层嵌套式判断:
  1. if(user != null){
  2.     Wallet wallet = user.getWallet();
  3.     if(wallet != null){
  4.         BigDecimal balance = wallet.getBalance();
  5.         if(balance != null){
  6.             // 实际业务逻辑
  7.         }
  8.     }
  9. }

这种写法既不优雅又影响代码可读性。
那么,我们该如何优化呢?

最近看机会的小伙伴,可以看下这个。
技术大厂,待遇给的还可以,就是偶尔有加班(放心,加班有加班费)。前、后端/测试,多地缺人,感兴趣的可以来~


二、Java 8+时代的判空革命
Java8之后,新增了Optional类,它是用来专门判空的。
能够帮你写出更加优雅的代码。
1. Optional黄金三板斧
  1. // 重构后的链式调用
  2. BigDecimal result = Optional.ofNullable(user)
  3.     .map(User::getWallet)
  4.     .map(Wallet::getBalance)
  5.     .map(balance -> balance.add(new BigDecimal("100")))
  6.     .orElse(BigDecimal.ZERO);

高级用法:条件过滤
  1. Optional.ofNullable(user)
  2.     .filter(u -> u.getVipLevel() > 3)
  3.     .ifPresent(u -> sendCoupon(u)); // VIP用户发券

2. Optional抛出业务异常
  1. BigDecimal balance = Optional.ofNullable(user)
  2.     .map(User::getWallet)
  3.     .map(Wallet::getBalance)
  4.     .orElseThrow(() -> new BusinessException("用户钱包数据异常"));

3. 封装通用工具类
  1. public class NullSafe {
  2.    
  3.     // 安全获取对象属性
  4.     public static <T, R> R get(T target, Function<T, R> mapper, R defaultValue) {
  5.         return target != null ? mapper.apply(target) : defaultValue;
  6.     }
  7.    
  8.     // 链式安全操作
  9.     public static <T> T execute(T root, Consumer<T> consumer) {
  10.         if (root != null) {
  11.             consumer.accept(root);
  12.         }
  13.         return root;
  14.     }
  15. }

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

三、现代化框架的判空银弹4. Spring实战技巧
Spring中自带了一些好用的工具类,比如:CollectionUtils、StringUtils等,可以非常有效的进行判空。
具体代码如下:
/
  1. / 集合判空工具
  2. List<Order> orders = getPendingOrders();
  3. if (CollectionUtils.isEmpty(orders)) {
  4.     return Result.error("无待处理订单");
  5. }

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

5. Lombok保驾护航
我们在日常开发中的entity对象,一般会使用Lombok框架中的注解,来实现getter/setter方法。
其实,这个框架中也提供了@NonNull等判空的注解。
比如:
  1. @Getter
  2. @Setter
  3. public class User {
  4.     @NonNull // 编译时生成null检查代码
  5.     private String name;
  6.    
  7.     private Wallet wallet;
  8. }

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

四、工程级解决方案6. 空对象模式
  1. public interface Notification {
  2.     void send(String message);
  3. }

  4. // 真实实现
  5. public class EmailNotification implements Notification {
  6.     @Override
  7.     public void send(String message) {
  8.         // 发送邮件逻辑
  9.     }
  10. }

  11. // 空对象实现
  12. public class NullNotification implements Notification {
  13.     @Override
  14.     public void send(String message) {
  15.         // 默认处理
  16.     }
  17. }

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

7. Guava的Optional增强
其实Guava工具包中,给我们提供了Optional增强的功能。
比如:
  1. import com.google.common.base.Optional;

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

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

Guava工具包中的Optional类已经封装好了,我们可以直接使用。
五、防御式编程进阶8. Assert断言式拦截
其实有些Assert断言类中,已经做好了判空的工作,参数为空则会抛出异常。
这样我们就可以直接调用这个断言类。
例如下面的ValidateUtils类中的requireNonNull方法,由于它内容已经判空了,因此,在其他地方调用requireNonNull方法时,如果为空,则会直接抛异常。
我们在业务代码中,直接调用requireNonNull即可,不用写额外的判空逻辑。
例如:
  1. public class ValidateUtils {
  2.     public static <T> T requireNonNull(T obj, String message) {
  3.         if (obj == null) {
  4.             throw new ServiceException(message);
  5.         }
  6.         return obj;
  7.     }
  8. }

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

9. 全局AOP拦截
我们在一些特殊的业务场景种,可以通过自定义注解 + 全局AOP**的方式,来实现实体或者字段的判空。
例如:
  1. @Aspect
  2. @Component
  3. public class NullCheckAspect {
  4.    
  5.     @Around("@annotation(com.xxx.NullCheck)")
  6.     public Object checkNull(ProceedingJoinPoint joinPoint) throws Throwable {
  7.         Object[] args = joinPoint.getArgs();
  8.         for (Object arg : args) {
  9.             if (arg == null) {
  10.                 throw new IllegalArgumentException("参数不可为空");
  11.             }
  12.         }
  13.         return joinPoint.proceed();
  14.     }
  15. }

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

六、实战场景对比分析场景1:深层次对象取值
  1. // 旧代码(4层嵌套判断)
  2. if (order != null) {
  3.     User user = order.getUser();
  4.     if (user != null) {
  5.         Address address = user.getAddress();
  6.         if (address != null) {
  7.             String city = address.getCity();
  8.             // 使用city
  9.         }
  10.     }
  11. }

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

场景2:批量数据处理
  1. List<User> users = userService.listUsers();

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

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

七、性能与安全的平衡艺术
上面介绍的这些方案都可以使用,但除了代码的可读性之外,我们还需要考虑一下性能因素。
下面列出了上面的几种在CPU消耗、内存只用和代码可读性的对比:
方案
CPU消耗
内存占用
代码可读性
适用场景
多层if嵌套
★☆☆☆☆
简单层级调用
Java Optional
★★★★☆
中等复杂度业务流
空对象模式
★★★★★
高频调用的基础服务
AOP全局拦截
★★★☆☆
接口参数非空验证
黄金法则
  • Web层入口强制参数校验
  • Service层使用Optional链式处理
  • 核心领域模型采用空对象模式
八、扩展技术
除了,上面介绍的常规判空之外,下面再给大家介绍两种扩展的技术。
Kotlin的空安全设计
虽然Java开发者无法直接使用,但可借鉴其设计哲学:
val city = order?.user?.address?.city ?: "default"JDK 14新特性预览
  1. // 模式匹配语法尝鲜
  2. if (user instanceof User u && u.getName() != null) {
  3.     System.out.println(u.getName().toUpperCase());
  4. }

总之,优雅判空不仅是代码之美,更是生产安全底线。
本文分享了代码判空的10种方案,希望能够帮助你编写出既优雅又健壮的Java代码。


您需要登录后才可以回帖 登录 | 注册

本版积分规则

247

主题

257

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部

247

主题

257

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部