被 Java 的 NullPointerException 折磨 3 天,最后用 Optional 解决,附代码
本文讲述了作者被 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 主要出现在几个地方:
- currentProduct.getCategoryId()处,提示 currentProduct 为 null。
- p.getBrandId().equals(currentProduct.getBrandId())处,提示 p.getBrandId () 为 null 或者 currentProduct.getBrandId () 为 null。
- 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 问题再也没有出现过,推荐功能也能正常工作。
(三)代码解析
- 首先,使用Optional.ofNullable(productService.getById(productId))将获取到的 currentProduct 包装成 Optional 实例,这样就可以避免 currentProduct 为 null 时的 NPE。
- 然后,通过map(currentProduct -> currentProduct.getCategoryId())提取 categoryId,如果 currentProduct 为 null,这一步会返回一个空的 Optional 实例。
- 接着,使用map(categoryId -> productService.getByCategoryId(categoryId))查询同分类商品,如果 categoryId 为 null,这一步也会返回空的 Optional 实例。
- 对于查询到的同分类商品列表,使用flatMap方法进行处理,先过滤掉 null 的商品,然后通过嵌套的 Optional 操作,对商品的 brandId 和 price 进行判断和过滤,避免了因这些字段为 null 而导致的 NPE。
- 最后,使用orElse(Collections.emptyList())指定如果任何一步出现 null,就返回空列表。
五、总结归纳
(一)问题解决回顾
在这次经历中,我从最初被 NPE 折磨得焦头烂额,到尝试用传统的 null 判断解决问题但导致代码臃肿,再到最后借助 Optional 类优雅地解决问题,深刻体会到了 Optional 类在处理空指针问题上的优势。
(二)Optional 类的优势
- 减少 NPE 的发生:通过 Optional 类的方法,可以避免显式的 null 判断,从而减少因疏忽而导致的 NPE。
- 提高代码可读性:Optional 类的方法名直观地表达了对值的处理逻辑,让代码更容易理解。
- 增强代码可维护性:使用 Optional 类可以使代码结构更清晰,减少了大量的 if-else 判断,便于后续的修改和维护。
(三)使用 Optional 的注意事项
- 不要滥用 Optional:虽然 Optional 类很有用,但并不是所有的 null 判断都需要用 Optional 来处理,对于简单的情况,显式的 null 判断可能更直观。
- 避免在构造方法、属性中使用 Optional:Optional 类主要用于方法的返回值,不建议在类的属性或构造方法中使用。
- 正确使用 Optional 的方法:要理解 Optional 类中各个方法的作用和区别,避免因使用不当而导致问题。
总之,Optional 类为我们解决空指针问题提供了一种新的思路和方法,合理地使用它可以让我们的 Java 代码更加健壮、优雅。在今后的开发中,我会继续深入学习和运用 Optional 类,同时也会不断总结经验,提高自己的开发水平。
更多推荐


所有评论(0)