add()方法导致NPE?不可变集合singletonList的隐藏陷阱!

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 大家好,我是小米。本文分享了在真实工作场景中排查NPE(NullPointerException)异常的过程。测试环境中打开退单详情时页面崩溃,NPE出现在调用集合的`add()`方法时。通过日志定位和源码分析,最终发现问题是由于使用了`Collections.singletonList()`创建的不可变集合导致的。我们将其替换为可变集合`ArrayList`,成功解决了问题。希望这篇文章能帮助大家更好地处理类似异常。



大家好呀~我是小米,今天来给大家分享一个真实工作场景中排查NPE(NullPointerException)异常的过程。昨天,测试童鞋找到我们,说在测试环境中打开退单详情时,页面直接崩了!NPE报错突然出现,而且竟然是在调用集合的add()方法时出问题了。为了不影响后续测试进度,我们立刻投入排查,最终快速定位到了问题代码,并解决了问题。接下来,带大家一起回顾下我们是如何排查和修复这个问题的,希望对大家有所帮助!

NPE报错分析:一步步锁定异常原因

1. 接到报错请求

测试环境的退单详情页面无法打开,直接报NPE异常。在这种情况下,我们首先要了解报错的具体场景,以便重现并分析问题。

  • 场景描述:测试人员在测试环境中打开退单详情,页面直接报错。
  • 初步猜测:一般情况下,退单详情报错大概率出现在数据初始化、调用方法过程或者前后端数据交互过程中。

2. 查看日志信息

为了定位错误点,我们迅速打开了日志,通过日志找到相关信息。看到如下报错信息:

通过日志定位,我们很快找到了出错的具体位置。在代码中,是一个对集合进行add()操作的地方发生了NPE。

3. 锁定代码片段

打开代码后,我们找到了对应的错误代码。错误的代码大概如下所示:

通过代码来看,returnItemList是通过Collections.singletonList(returnItemVO)创建的。显然,后续在调用add()方法时出现了NPE异常。

深入分析:为什么Collections.singletonList()导致NPE?

看到Collections.singletonList()add()方法的组合出错,我们先来分析一下这个singletonList到底是个什么鬼!看名字singletonList,似乎代表“单例集合”的意思,那它究竟是什么特性导致了NPE呢?

什么是Collections.singletonList()?

Collections.singletonList(T o)是JDK集合工具类中的一个静态方法,用于返回一个只有一个元素的不可变集合。简单来说,它构造了一个只能包含单个元素的集合,而且这个集合是不可修改的。

为什么调用add()会报错?

singletonList()方法返回的是一个特殊的实现类:SingletonList,该类的add()方法是由抽象类AbstractList实现的。由于这是一个不可变集合,add()操作会直接抛出UnsupportedOperationException。因此在上面的代码中,returnItemList.add(new ReturnItemVO())直接触发了NPE。

因此问题根源在于Collections.singletonList()的特性,这一特性导致集合在被创建后无法进行修改操作。

Collections.singletonList()的实现细节

为了帮助大家更深入地理解这个报错,让我们进一步分析singletonList的实现细节。通过阅读源码,我们可以看到singletonList返回的是一个由AbstractList抽象类实现的不可变集合。源码中的核心代码如下:

SingletonList类的内部,并没有实现add()方法,而是通过父类AbstractList的默认实现来实现。因此调用add()会抛出UnsupportedOperationException

特性总结

Collections.singletonList()的几个主要特点:

  • 只包含一个元素,无法添加或删除其他元素。
  • 不可变集合,任何修改操作都会抛出UnsupportedOperationException。

解决方案:使用可变集合

既然singletonList无法满足我们的需求,那我们该如何修改代码呢?要解决这个问题,最简单的方式是使用一个可以正常操作的可变集合,比如ArrayList。可以通过如下代码来替换:

这样一来,我们就将不可变集合替换为了可变的ArrayList,成功规避了add()方法的报错。

修改后的代码

我们将原始代码中的不可变集合Collections.singletonList()替换为Google Guava中的Lists.newArrayList()来创建一个可变集合。代码修改如下:

这里我们使用了Lists.newArrayList(returnItemVO)来创建一个新的ArrayList,初始化时包含returnItemVO对象,同时可以正常执行add()方法。

测试验证

修改完代码后,我们立刻在测试环境中重新部署并验证了一下,结果显示问题已经完全解决,退单详情页面可以正常打开了!

总结与反思

  • 正确选择集合类型:Collections.singletonList()创建的是不可变集合,在尝试对其进行添加或删除操作时会直接报错。因此在实际开发中,我们要慎用不可变集合,特别是在需要动态增删集合元素的场景下,应该优先选择ArrayList等可变集合。
  • 掌握集合工具类的特性:Collections类提供了很多方便的方法来创建集合,像singletonList()、emptyList()等不可变集合的创建方法在某些特殊场景下非常有用,但同时也容易引发NPE等异常。因此要熟悉并掌握工具类的特性和局限,避免不必要的坑。
  • 日志定位与源码阅读的重要性:在实际工作中,异常处理不仅要依赖IDE调试工具,还需要通过日志快速定位问题,找到问题代码后,通过阅读相关类和方法的源码,进一步确定问题的根本原因,这样才能更快速地解决问题。

这次的NPE问题给我们团队一个小小的提醒:即使是一个简单的集合类型选择也可能引发应用级别的问题。在今后的开发中,我们会更加注意代码细节,避免不必要的Bug。希望这篇文章能够帮助到大家在工作中更好地处理类似的异常问题!

我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号软件求生,获取更多技术干货!

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
【BUG】循环中重复使用对象一定要注意
【BUG】循环中重复使用对象一定要注意
|
2月前
|
JavaScript 前端开发
判断对象是否含有改属性,三个方法
JavaScript中判断对象是否包含属性的三种方法:1. 使用`'property' in object`检查自有属性和继承属性;2. 使用`object.hasOwnProperty('property')`仅检查自有属性;3. 使用`if (object.property)`判断,但返回属性值。
36 2
判断对象是否含有改属性,三个方法
|
3月前
|
编译器
【Bug记录】list模拟实现const迭代器类
【Bug记录】list模拟实现const迭代器类
|
3月前
【Azure Developer】使用PowerShell Where-Object方法过滤多维ArrayList时候,遇见的诡异问题 -- 当查找结果只有一个对象时,返回结果修改了对象结构,把多维变为一维
【Azure Developer】使用PowerShell Where-Object方法过滤多维ArrayList时候,遇见的诡异问题 -- 当查找结果只有一个对象时,返回结果修改了对象结构,把多维变为一维
|
存储 编译器 Go
Go语言隐藏的接口陷阱:nil值判断的各种误区
Go语言隐藏的接口陷阱:nil值判断的各种误区
166 0
|
6月前
this的含义,什么情况下使用this,改变this指针的两种办法。 === 由于this关键字很混乱,如何解决这个问题
this的含义,什么情况下使用this,改变this指针的两种办法。 === 由于this关键字很混乱,如何解决这个问题
43 0
|
C++ 容器
C++ vector 赋值、删除、排序类之外的其他函数
C++ vector 赋值、删除、排序类之外的其他函数
117 0
|
JSON Java 数据库
代码重构实战-将值对象改为引用对象(Change Value to Reference)
一个数据结构中可能包含多个记录,而这些记录都关联到同一个逻辑数据结构。例如,我可能会读取一系列订单数据,其中有多条订单属于同一个顾客。遇到这样的共享关系,既能将顾客信息作为值对象看待,也能将其视为引用对象
102 0
|
安全 Java 程序员
还在重复写空指针检查代码?考虑使用 Optional 吧!
如果要给 Java 所有异常弄个榜单,我会选择将 NullPointerException 放在榜首。这个异常潜伏在代码中,就像个遥控炸弹,不知道什么时候这个按钮会被突然按下(传入 null 对象)。 还记得刚入行程序员的时候,三天两头碰到空指针异常引发的 Bug,解决完一个,又在另一处碰到。那时候师兄就教我,不要相信任何『对象』,特别是别人给你的,这些地方都加上判断。于是代码通常为会变成下面这样:
还在重复写空指针检查代码?考虑使用 Optional 吧!
|
Java
【Groovy】集合遍历 ( 调用集合的 every 方法判定集合中的所有元素是否符合闭包规则 | =~ 运算符等价于 contains 函数 | 代码示例 )
【Groovy】集合遍历 ( 调用集合的 every 方法判定集合中的所有元素是否符合闭包规则 | =~ 运算符等价于 contains 函数 | 代码示例 )
191 0
【Groovy】集合遍历 ( 调用集合的 every 方法判定集合中的所有元素是否符合闭包规则 | =~ 运算符等价于 contains 函数 | 代码示例 )