java中list使用时需避免的场景总结
作者:毅航
众所周知,Java
为开发者提供了多种集合类的实现。而Java
的集合类通常被分为两大类:Map
和Collection
。进一步,Collection
又分为三个子类,包括List、Set和Queue
。其中,可以几乎所有业务代码都需要用到List
,因此其也被认为是最为是集合中最重要的一个结构。
但List
的错误使用也会导致诸多问题,今天我们就来看一看几个错误使用List
的场景。
前言
在日常的Java
面试中,ArrayList
绝对可以算得上是Java
后端面试中的一个常客。为此网上也有很多文章来对ArrayList
的源码进行分析,其实重点归结下来无非以下几点
ArrayList
中元素增、删操作的原理;ArrayList
中的扩容机制,元素拷贝原理;ArrayList
线程安全性以及fast-fail
机制等。
在面试中可能你对于上述问题可以做到对答如流,但是真正落实到实际开发中你能保证写出没有Bug
的代码吗?对于这个问题不同的开发者
可能有着不同的答案,但无论你的答案是什么,笔者还是希望你能花几分钟读一读后续的内容,相信读完你一定会有所收获~~~
删除元素时传递的参数到底是什么
在开始分析之前,我们先来看一段代码:
@Test public void testStrList() { ArrayList<String> strs = new ArrayList<>(); strs.add("hello"); strs.add("coding"); strs.add("word"); strs.remove(1); log.info("Arrays after is [{}] ", Arrays.toString(strs.toArray())); }
上述代码逻辑很简单,无非就是向strs
中添加三个字符串
信息,然后调用remove
方法进行删除,那此时上述代码会打印出什么内容呢?此时,相信你肯定不假思索的回答出肯定是Arrays after is [[hello, word]]
。但是当我拿出如下这段代码,阁下又该如何应对呢?
@Test public void testList() { ArrayList<Integer> nums = new ArrayList<>(); for (int i = 0 ; i < 5 ; i ++) { nums.add(5-i); } // <1> nums.remove(new Integer(3)); nums.remove(3); log.info("Arrays after is [{}] ", Arrays.toString(nums.toArray())); }
对于上述代码,笔者有两个疑问:
- 上述代码会输出什么呢?
- 如果将上述代码
nums.remove(3)
注释掉,而将<1>
处代码注释打开,其输出结果又是什么呢?
这个问题归根到底本质就是对于ArrayList
中remove
方法的理解,可能面试
时你对于ArrayList
的remove
方法也能侃侃而谈,但面对如此场景你还能不假思索的回答出来吗?
有疑惑也别慌,接下来我们就来看看List
中关于remove
的方法有什么样的定义:
List # remove
public interface List<E> extends Collection<E> { ...... boolean remove(Object o); ...... E remove(int index); ...... }
可以看到,在List
接口中,对于remove
方法进行了重载,其支持两种类型的传参:一种是传入基本类型
;另一种则需要传入一个对象
。进一步,ArrayList
中对于不同参数下remove
方法的执行逻辑也是有差异的。具体来看:
- 当
remove
中传入的参数为int
时,则会删除参数
对应索引
中的元素; - 当
remove
中传入的参数为Object
时,则会遍历列表中的元素
,进而删除列表中的对应元素
。
至此,上述代码执行后的结果其实已经很明显了。当执行nums.remove(3)
时,其会删除对应索引下的的元素,即删除列表中的元素2
;而当执行nums.remove(new Integer(3))
时,则会删除列表中的元素3
。
可能你会觉得,这里无非就是remove
方法有一个重载逻辑罢了,有何难?你无非就是比我看的时候细心
了一点罢了,这又能算的上什么呢?
如果你有这样的想法,那不妨来看看如下这段代码,想一想下列代码最终输出的size
是多少呢?
public void testStrList() { ArrayList<String> strs = new ArrayList<>(); strs.add("hello"); strs.add("coding"); strs.add("word"); // 定义匹配规则 ArrayList<String> mapStrs = new ArrayList<>(Arrays.asList("hello")); // 存储适配信息坐标 ArrayList<Integer> index = new ArrayList<>(); for (int i = 0 ; i < strs.size() ; i ++) { if (mapStrs.contains(strs.get(i))) { index.add(i); } } // 删除匹配信息 for (int i = 0 ; i < index.size() ; i ++) { strs.remove(index.get(i)); } log.info("strs removed size is [{}] ", strs.size()); }
上述代码最终会输出strs removed size is 3
,不知你的答案是否是这样的?至于为何是这样,笔者在此就不进行分析了。这算是笔者留的一个思考题,欢迎你在评论区留言。
Arrays.asList你用对了吗
众所周知,Arrays.asList
是 Java
中的一个静态方法,它的主要作用是将一个数组转换成一个固定大小的 List
。 那下述两种方式构建的List
有什么差异呢?
int[] arr = {1, 2, 3}; // <1> 传入数组构建List List listArray = Arrays.asList(arr); // <2> 通过参数构建List List list = Arrays.asList(1,2,3);
执行代码你会发现,方法<1>
这样初始化的 List
并不是我们期望的包含 3
个数字的 List
。而其会创建一个元素类型为[]int
的List
。换言之,其最终会生成的 List
是一个元素个数为 1
的整型数组。
那为什么会这样?我们来看下其源码:
public static List asList(T... a) { return new ArrayList<>(a); }
进一步,其在构建ArrayList
时的逻辑如下所示:
通过追踪源码可以看到,Arrays.asList
方法传入的是一个泛型T
类型可变参数,而我们传入的int[]
最终作为了一个对象成为了泛型类型 T
。当然,这个问题更深层的原因在于:Jdk
在对象的封箱/拆箱只支持将 int
装箱为 Integer
,却不能把 int 数组
装箱为 Integer 数组
。进一步,如下代码则不会存在此问题。
Integer[] arr2 = {1, 2, 3}; List list = Arrays.asList(arr2);
总之, Arrays.asList
不能直接使用来转换基本类型数组。 进一步,既然Arrays.asList
可以得到一个List
,那对其进行add、remove
等操作是不是没啥问题?。例如如下这样
Integer[] arr2 = {1, 2, 3}; List list = Arrays.asList(arr2); list.add(3,4)
如果运行上述代码你会喜提一个UnsupportedOperationException
。换言之,为list
中的3
号位置新增整型 4
的操作失败了。造成这一问题的在于:Arrays.asList
返回的List
不支持增删操作。即Arrays.asList
返回的 List
并不是 我们期望的 java.util.ArrayList
。其最终会返回 Arrays
的内部类 ArrayList
。而该类并没有覆写父类AbstractList
的add
方法,而父类中add
方法的实现,就是抛出 UnsupportedOperationException
。相关代码如下:
AbstractList # add
public void add(int index, E element) { throw new UnsupportedOperationException(); }
总结
至此,我们抛砖引玉的介绍了使用List
时可能遇到的一些小问题。事实上,除了上述讨论的问题外List
在使用切片功能也可能导致一些意想不到的OOM
异常,在此笔者就不逐一进行分析了,感兴趣的读者可翻阅相关资料进行了解。
或许,List
使用过程中的这些小细节在你看来无关紧要,但细节决定成败。当然,这也是一个仁者见仁智者的问题,笔者在此就不赘述了。
到此这篇关于java中list使用时需避免的场景总结的文章就介绍到这了,更多相关java list内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!