|
一、传统判空的血泪史
某互联网金融平台因费用计算层级的空指针异常,导致凌晨产生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){ // 实际业务逻辑 } }}这种写法既不优雅又影响代码可读性。
那么,我们该如何优化呢?
最近准备面试的小伙伴,可以看一下这个宝藏网站:www.susan.net.cn,里面:面试八股文、面试真题、工作内推什么都有。
二、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 { // 安全获取对象属性 public static <T, R> R get(T target, Function<T, R> mapper, R defaultValue) { return target != null ? mapper.apply(target) : defaultValue; } // 链式安全操作 public static <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@Setterpublic class User { @NonNull // 编译时生成null检查代码 private String name; private Wallet wallet;}// 使用构造时自动判空User user = new User(@NonNull "张三", wallet);四、工程级解决方案
6. 空对象模式
public interface Notification { void send(String message);}// 真实实现public class EmailNotification implements Notification { @Override public void send(String message) { // 发送邮件逻辑 }}// 空对象实现public class 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;// 创建携带缺省值的OptionalOptional<User> userOpt = Optional.fromNullable(user).or(defaultUser);// 链式操作配合FunctionOptional<BigDecimal> amount = userOpt.transform(u -> u.getWallet()) .transform(w -> w.getBalance());Guava工具包中的Optional类已经封装好了,我们可以直接使用。
五、防御式编程进阶
8. Assert断言式拦截
其实有些Assert断言类中,已经做好了判空的工作,参数为空则会抛出异常。
这样我们就可以直接调用这个断言类。
例如下面的ValidateUtils类中的requireNonNull方法,由于它内容已经判空了,因此,在其他地方调用requireNonNull方法时,如果为空,则会直接抛异常。
我们在业务代码中,直接调用requireNonNull即可,不用写额外的判空逻辑。
例如:
public class ValidateUtils { public static <T> T requireNonNull(T obj, String message) { if (obj == null) { throw new ServiceException(message); } return obj; }}// 使用姿势User currentUser = ValidateUtils.requireNonNull( userDao.findById(userId), "用户不存在-ID:" + userId);9. 全局AOP拦截
我们在一些特殊的业务场景种,可以通过自定义注解 + 全局AOP拦截器的方式,来实现实体或者字段的判空。
例如:
@Aspect@Componentpublic class NullCheckAspect { @Around("@annotation(com.xxx.NullCheck)") public Object checkNull(ProceedingJoinPoint joinPoint) throws Throwable { Object[] args = joinPoint.getArgs(); for (Object arg : args) { if (arg == null) { throw new 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代码。
最后说一句(求关注,别白嫖我)
如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。 |
|