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日志并进行多维度分析。
相关文章
|
4月前
|
编译器
【Bug记录】list模拟实现const迭代器类
【Bug记录】list模拟实现const迭代器类
|
存储 编译器 Go
Go语言隐藏的接口陷阱:nil值判断的各种误区
Go语言隐藏的接口陷阱:nil值判断的各种误区
195 0
ES6新增循环对象的四种方法(通俗易懂)
ES6新增循环对象的四种方法(通俗易懂)
|
7月前
|
C语言
C语言函数传递了指针,值没有被修改的原因及解决方法
C语言函数中传递了指针作为参数,确切来说是传递了指向变量的内存地址作为参数,可经过函数内的修改之后,该指针指向的变量的值为什么不会被修改?就像下方这个函数:
123 1
|
7月前
this的含义,什么情况下使用this,改变this指针的两种办法。 === 由于this关键字很混乱,如何解决这个问题
this的含义,什么情况下使用this,改变this指针的两种办法。 === 由于this关键字很混乱,如何解决这个问题
47 0
#PY小贴士# for 循环定义的变量,循环外可以用吗?
我们知道,在 python 中要获取一个变量的值,必须是先给它赋值过,不然就是未定义。那么这个 i,代码中没有显式的赋值,在循环体之外还可以用吗?
java基础学习 数组,循环,变量,函数加载情况先后顺序,方法定义
java基础学习 数组,循环,变量,函数加载情况先后顺序,方法定义
java基础学习 数组,循环,变量,函数加载情况先后顺序,方法定义
|
Java
java面试题:当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
答:是值传递。Java编程语言只有值传递参数。 当一个对象实例作为一个参数被传递到方法中时,参数的值就是该对象的引用一个副本。指向同一个对象,对象的内容可以在被调用的方法中改变,但对象的引用(不是引用的副本)是永远不会改变的。
12616 0
ES6中箭头函数 (=>)、三点运算符(...)的基本用法和注意事项(this指向)
学习ES6中箭头函数 (=>)、三点运算符(...)的基本用法和注意事项(this指向)。
162 0
|
Java
【Groovy】集合遍历 ( 调用集合的 every 方法判定集合中的所有元素是否符合闭包规则 | =~ 运算符等价于 contains 函数 | 代码示例 )
【Groovy】集合遍历 ( 调用集合的 every 方法判定集合中的所有元素是否符合闭包规则 | =~ 运算符等价于 contains 函数 | 代码示例 )
207 0
【Groovy】集合遍历 ( 调用集合的 every 方法判定集合中的所有元素是否符合闭包规则 | =~ 运算符等价于 contains 函数 | 代码示例 )