本文讲述了作者被 Java 中的 NullPointerException(空指针异常)折磨 3 天的经历,详细介绍了问题出现的场景、排查过程中遇到的困难,以及最终如何运用 Optional 类成功解决问题。文中不仅分享了具体的代码实现,还深入解析了 Optional 的原理和使用方法,旨在为其他开发者提供借鉴,帮助大家在遇到类似空指针问题时,能更高效地运用 Optional 类规避和解决,减少开发中的困扰。​

一、引言:空指针异常的 “噩梦” 开端​

作为一名 Java 开发者,我一直认为自己对常见的异常处理还算得心应手,但最近一次被 NullPointerException(简称 NPE)折磨的经历,让我深刻体会到了这个 “千年老难题” 的厉害。那是一个普通的周一,我接手了一个团队之前开发的电商项目模块,需要在原有基础上增加一个商品详情页的推荐功能。本以为是个简单的需求,却没想到就此陷入了 NPE 的 “泥潭”,整整 3 天时间,我都在与这个棘手的异常斗智斗勇,直到最后借助 Optional 类才得以解决。​

二、问题出现:看似简单的代码暗藏危机​

这个电商项目的商品详情推荐功能,需要根据当前商品的分类、品牌、价格区间等信息,从数据库中筛选出相似的商品并展示。在代码实现上,首先需要通过商品 ID 获取商品的详细信息,然后根据商品信息中的分类 ID 查询该分类下的其他商品,再结合品牌和价格等条件进行过滤。​

最初的代码片段大致如下:​

public List<Product> getRecommendProducts(Long productId) {​

// 根据商品ID获取商品信息​

Product currentProduct = productService.getById(productId);​

// 获取商品分类ID​

Long categoryId = currentProduct.getCategoryId();​

// 查询该分类下的商品​

List<Product> sameCategoryProducts = productService.getByCategoryId(categoryId);​

// 过滤出符合条件的推荐商品​

List<Product> recommendProducts = sameCategoryProducts.stream()​

.filter(p -> p.getBrandId().equals(currentProduct.getBrandId()))​

.filter(p -> p.getPrice().compareTo(currentProduct.getPrice() * 0.8) >= 0​

在本地测试时,这段代码运行得还算顺利,推荐功能也能正常实现。但当代码部署到测试环境,进行更全面的测试时,问题出现了 —— 系统频繁抛出 NullPointerException。​

三、排查过程:与 NPE 的 “周旋”​

(一)初步定位异常位置​

拿到异常日志后,我首先查看了堆栈信息,发现 NPE 主要出现在几个地方:​

  1. currentProduct.getCategoryId()处,提示 currentProduct 为 null。​
  1. p.getBrandId().equals(currentProduct.getBrandId())处,提示 p.getBrandId () 为 null 或者 currentProduct.getBrandId () 为 null。​
  1. p.getPrice().compareTo(...)处,提示 p.getPrice () 为 null 或者 currentProduct.getPrice () 为 null。​

(二)逐一分析可能原因​

针对第一个异常位置,我推测可能是 productService.getById (productId) 方法在某些情况下返回了 null,比如传入的 productId 不存在于数据库中。为了验证这一点,我查看了数据库中是否存在对应的商品记录,发现确实有部分测试数据的 productId 是无效的,导致 currentProduct 为 null。​

对于第二个异常位置,可能是某些商品的 brandId 字段为 null,或者 currentProduct 的 brandId 为 null。通过查询数据库,我发现确实有部分商品没有设置 brandId,这就导致在调用 equals 方法时出现了 NPE。​

第三个异常位置的原因与第二个类似,部分商品的 price 字段为 null,在进行比较时就会抛出 NPE。​

(三)尝试传统解决方法​

找到原因后,我开始尝试用传统的方式解决,也就是添加大量的 null 判断。修改后的代码如下:​

虽然添加了这么多的 null 判断后,NPE 的问题暂时得到了解决,但代码变得臃肿不堪,可读性和可维护性大大降低。而且,我总觉得这种方式不够优雅,还可能存在遗漏的 null 判断情况。​

四、柳暗花明:Optional 类的 “救赎”​

在一次团队技术分享会上,有同事提到了 Java 8 中引入的 Optional 类,说它可以很好地解决空指针问题,让代码更简洁、更安全。这让我眼前一亮,决定尝试用 Optional 类来重构代码。​

(一)Optional 类简介​

Optional 类是一个容器类,它可以保存类型 T 的值,代表这个值存在;或者仅仅保存 null,表示这个值不存在。通过使用 Optional 类,我们可以避免显式的 null 判断,从而减少 NPE 的发生。​

Optional 类提供了许多实用的方法,比如:​

  • of(T value):创建一个 Optional 实例,value 不能为 null。​
  • ofNullable(T value):创建一个 Optional 实例,value 可以为 null。​
  • isPresent():判断 Optional 实例中是否包含值。​
  • get():如果 Optional 实例中包含值,则返回该值,否则抛出 NoSuchElementException。​
  • orElse(T other):如果 Optional 实例中包含值,则返回该值,否则返回 other。​
  • map(Function<? super T, ? extends U> mapper):如果 Optional 实例中包含值,则对该值应用 mapper 函数,并返回一个新的 Optional 实例。​
  • flatMap(Function<? super T, Optional<U>> mapper):与 map 方法类似,但 mapper 函数返回的是一个 Optional 实例。​
  • filter(Predicate<? super T> predicate):如果 Optional 实例中包含值,且该值满足 predicate 条件,则返回该 Optional 实例,否则返回一个空的 Optional 实例。​

(二)用 Optional 重构代码​

结合项目中的问题,我开始用 Optional 类重构代码。重构后的代码如下:​

写完这段代码后,我发现代码变得简洁了很多,不再有大量的显式 null 判断,而且逻辑也更加清晰。在测试环境中运行后,之前的 NPE 问题再也没有出现过,推荐功能也能正常工作。​

(三)代码解析​

  1. 首先,使用Optional.ofNullable(productService.getById(productId))将获取到的 currentProduct 包装成 Optional 实例,这样就可以避免 currentProduct 为 null 时的 NPE。​
  1. 然后,通过map(currentProduct -> currentProduct.getCategoryId())提取 categoryId,如果 currentProduct 为 null,这一步会返回一个空的 Optional 实例。​
  1. 接着,使用map(categoryId -> productService.getByCategoryId(categoryId))查询同分类商品,如果 categoryId 为 null,这一步也会返回空的 Optional 实例。​
  1. 对于查询到的同分类商品列表,使用flatMap方法进行处理,先过滤掉 null 的商品,然后通过嵌套的 Optional 操作,对商品的 brandId 和 price 进行判断和过滤,避免了因这些字段为 null 而导致的 NPE。​
  1. 最后,使用orElse(Collections.emptyList())指定如果任何一步出现 null,就返回空列表。​

五、总结归纳​

(一)问题解决回顾​

在这次经历中,我从最初被 NPE 折磨得焦头烂额,到尝试用传统的 null 判断解决问题但导致代码臃肿,再到最后借助 Optional 类优雅地解决问题,深刻体会到了 Optional 类在处理空指针问题上的优势。​

(二)Optional 类的优势​

  1. 减少 NPE 的发生:通过 Optional 类的方法,可以避免显式的 null 判断,从而减少因疏忽而导致的 NPE。​
  1. 提高代码可读性:Optional 类的方法名直观地表达了对值的处理逻辑,让代码更容易理解。​
  1. 增强代码可维护性:使用 Optional 类可以使代码结构更清晰,减少了大量的 if-else 判断,便于后续的修改和维护。​

(三)使用 Optional 的注意事项​

  1. 不要滥用 Optional:虽然 Optional 类很有用,但并不是所有的 null 判断都需要用 Optional 来处理,对于简单的情况,显式的 null 判断可能更直观。​
  1. 避免在构造方法、属性中使用 Optional:Optional 类主要用于方法的返回值,不建议在类的属性或构造方法中使用。​
  1. 正确使用 Optional 的方法:要理解 Optional 类中各个方法的作用和区别,避免因使用不当而导致问题。​

总之,Optional 类为我们解决空指针问题提供了一种新的思路和方法,合理地使用它可以让我们的 Java 代码更加健壮、优雅。在今后的开发中,我会继续深入学习和运用 Optional 类,同时也会不断总结经验,提高自己的开发水平。​

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐