Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
52944 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 48 讲
代码篇 (23讲)
Java 业务开发常见错误 100 例
15
15
1.0x
00:00/00:00
登录|注册

10 | 集合类:坑满地的List列表操作

Queue
Set
List
循环遍历List,调用remove方法删除元素,往往会遇到ConcurrentModificationException异常,原因是什么,修复方式又是什么呢?
调用类型是Integer的ArrayList的remove方法删除元素,传入一个Integer包装类的数字和传入一个int基本类型的数字,结果一样吗?
LinkedList性能不一定优于ArrayList
空间换时间 vs. 时间换空间
使用HashMap优化大List搜索性能
子List强引用原始List导致OOM
返回的子List不是一个普通的ArrayList
对原始数组的修改会影响到获得的List
Arrays.asList返回的List不支持增删操作
不能直接使用Arrays.asList来转换基本类型数组
Collection
Map
Stack
Graph
Tree
Queue
Map
Set
List
程序=数据结构+算法
思考与讨论
过于迷信教科书的大O时间复杂度
一定要让合适的数据结构做合适的事情
使用List.subList进行切片操作
Arrays.asList转换为List的坑
Java集合类
常见的数据结构
数据结构的重要性
集合类:坑满地的List列表操作

该思维导图由 AI 生成,仅供参考

你好,我是朱晔。今天,我来和你说说 List 列表操作有哪些坑。
Pascal 之父尼克劳斯 · 维尔特(Niklaus Wirth),曾提出一个著名公式“程序 = 数据结构 + 算法”。由此可见,数据结构的重要性。常见的数据结构包括 List、Set、Map、Queue、Tree、Graph、Stack 等,其中 List、Set、Map、Queue 可以从广义上统称为集合类数据结构。
现代编程语言一般都会提供各种数据结构的实现,供我们开箱即用。Java 也是一样,比如提供了集合类的各种实现。Java 的集合类包括 Map 和 Collection 两大类。Collection 包括 List、Set 和 Queue 三个小类,其中 List 列表集合是最重要也是所有业务代码都会用到的。所以,今天我会重点介绍 List 的内容,而不会集中介绍 Map 以及 Collection 中其他小类的坑。
今天,我们就从把数组转换为 List 集合、对 List 进行切片操作、List 搜索的性能问题等几个方面着手,来聊聊其中最可能遇到的一些坑。

使用 Arrays.asList 把数据转换为 List 的三个坑

Java 8 中 Stream 流式处理的各种功能,大大减少了集合类各种操作(投影、过滤、转换)的代码量。所以,在业务开发中,我们常常会把原始的数组转换为 List 类数据结构,来继续展开各种 Stream 操作。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了在List操作中可能遇到的一些常见问题,重点围绕使用Arrays.asList将数组转换为List时可能出现的类型问题和不支持增删操作的情况展开讨论。文章通过具体的代码示例和相关源码解释了这些问题的原因和解决方法,帮助读者更好地理解List操作中的坑,并避免在实际开发中遇到类似的问题。此外,文章还提到了使用List.subList进行切片操作可能导致OOM的问题,通过分析ArrayList的源码和实验结果,解释了子List和原始List相互影响的原因,并给出了两种修复方式。总的来说,本文内容涵盖了List操作中的常见问题和解决方法,对于开发人员来说具有一定的参考价值。 文章还介绍了在使用List集合类时可能遇到的两个常见误区。第一个是在大List进行单值搜索时,可以考虑使用HashMap以获得更好的性能优势;另一个是对于大量的元素插入、很少的随机访问的业务场景下,实际测试结果显示ArrayList在性能上几乎都优于LinkedList。这些实例和测试结果提醒读者不要迷信教科书的理论,而是在实际应用中进行测试和评估,以获得更准确的结论。 通过具体的代码示例和实验结果,本文帮助读者更好地理解List操作中的常见问题和解决方法,同时提醒读者在选择数据结构时要根据实际场景进行评估,避免盲目迷信理论。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 业务开发常见错误 100 例》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(47)

  • 最新
  • 精选
  • Darren
    置顶
    哈哈,好巧,前两年有段时间比较闲,研究ArrayList和LinkedList,也对于所谓的ArrayList查询快,增删慢以及LinkedList查询慢,增删快提出过疑问,也做过类似的实验,然后去年给19年校招生入职培训的时候还专门分享过。要打破常规思维,多问为什么,要多听多看,多实验。 回答下问题: 1、int类型是index,也就是索引,是按照元素位置删除的;Integer是删除某个元素,内部是通过遍历数组然后对比,找到指定的元素,然后删除;两个都需要进行数组拷贝,是通过System.arraycopy进行的 2、以foreach为例说,遍历删除实质是变化为迭代器实现,不管是迭代器里面的remove()还是next()方法,都会checkForComodification();而这个方法是判断modCount和expectedModCount是否相等,这个modCount是这个list集合修改的次数,每一次add或者remove都会增加这个变量,然后迭代器每次去next或者去remove的时候检查checkForComodification();发现expectedModCount(这个迭代器修改的次数)和modCount(这个集合实际修改的次数)不相等,就会抛出ConcurrentModificationException,迭代器里面没有add方法,用迭代器时,可以删除原来集合的元素,但是!一定要用迭代器的remove方法而不是集合自身的remove方法,否则抛异常。

    作者回复: 点赞

    2020-03-31
    4
    104
  • eazonshaw
    思考题: 1. 不一样。使用 ArrayList 的 remove方法,如果传参是 Integer类型的话,表示的是删除元素,如果传参是int类型的话,表示的是删除相对应索引位置的元素。 同时,做了个小实验,如果是String类型的ArrayList,传参是Integer类型时,remove方法只是返回false,视为元素不存在。 2. 原因:查看源码可以发现,remove方法会发生结构化修改,也就是 modCount 会增加。当循环过程中,比较当前 List 的 modCount 与初始的 modCount 不相等,就会报 ConcurrentModificationException。解决方法:1.使用 ArrayList 的迭代器 iterator,并调用之中的remove方法。查看源码可以发现,内部类的remove方法,会维护一个expectedModCount,使其与 ArrayList 的modCount保持一致。2.如果是java 8,可以使用removeIf方法进行删除操作。 ``` int expectedModCount = modCount; public void remove() { ... checkForComodification(); try { ... expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } ```

    作者回复: 这个回复挺有我的写作风格 😄

    2020-03-31
    39
  • 👽
    思考题2: 便利通常的实现方式for冒号的实现,其实底层还是用Iterator 删除元素,查看class文件大概是这样: Iterator var2 = list.iterator(); while(var2.hasNext()) { Integer integer = (Integer)var2.next(); list.remove(integer); } 删除元素后会调用next方法,next调用checkForComodification方法: final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } expectedModCount是初始化时,数组的modCount ,也就是说——初始化的数组长度和调用next方法时数组长度不一样时,就会ConcurrentModificationException,理论上讲,不仅仅remove,甚至是add也一样会报错。 尝试测试,将remove改为add: while(var2.hasNext()) { Integer integer = (Integer)var2.next(); list.add(integer); } 确实会报错。 知道了报错的原因,要修复倒也不难。 首先,摒弃for冒号,使用迭代器(其实迭代器也是for冒号) 既然,迭代器在List长度与迭代器初始化时识别到的List长度不一致就会报错。那就顺着它的意思来处理,每次List长度修改时,重新初始化迭代器。相当于长度重新初始化。 假设数组初始长度时10,形成的结果就是: Iterator 初始化 expectedModCount = 10; 然后删除某元素,数组长度9,Iterator 长度10,这时候如果调用next就会报错,所以,在这时候,重新初始化Iterator Iterator 长度初始化为9,与数组长度一致,就避免了报错。 代码实现如下: Iterator var2 = list.iterator(); while(var2.hasNext()) { Integer integer = (Integer)var2.next(); if (integer.equals(2)){ list.remove(integer); var2 = list.iterator(); } } 代码写的比较随意,可能存在纰漏。欢迎指点

    作者回复: 👍🏻,源码ListRemoveApplication中也有我的实现

    2020-03-31
    15
  • 失火的夏天
    1.remove包装类数字是删除对象,基本类型的int数字是删除下标。 2.好像是modcount和什么东西对不上来着,具体忘记了,看看其他大佬怎么说。解决这玩意就是改用迭代器遍历,调用迭代器的remove方法。 话说到这个linkedlist,真是感觉全面被arraylist压制。那这数据结构还留着干嘛呢?为什么不删掉算了。。。我个人感觉linekdlist只有在头尾加入删除元素的时候有一点点优势了吧。用队列或者双端队列的时候会偶然用到。但是感觉用对应的数组模式实现,效率会更高些,就是要考虑扩容的问题。 老师能帮忙解答一下linkedlist留下没删是因为什么吗?

    作者回复: 1. 从完备性角度说sdk需要这样的数据结构 2. 就这个数据结构本身实现上并无问题 3. 也完全不是一无是处任何场景都没有优势 还是留着吧

    2020-03-31
    2
    6
  • 👽
    感触颇深: Arrays的asList和subList,使用过程中需要谨慎,甚至可以考虑直接不用。 要熟悉数据结构。ArrayList 和 HashMap就是典型对比,ArrayList更适合随机访问,节约内存空间,大多数情况下性能不错。但,因为其本质上是数组,所以,无法实现快速找到想要的值。 LinkedList 没有想象中好用,使用前请考虑清楚。

    作者回复: 很不错的总结

    2020-03-31
    5
  • hellojd
    学习到了老师的探索精神,linedlist随机插入性能居然不高,刷新了认知。

    作者回复: :)

    2020-03-31
    5
  • 大大大熊myeh
    巧了,思考题1与我之前遇到的问题一样,List#remove方法竟然没删掉里面的元素,最后才发现原来是重载方法的锅,int是删List中该索引的元素,Integer是删除List中值为该Integer的元素。 当时还写了篇博客记录,恬不知耻的放上来:https://planeswalker23.github.io/2018/09/10/List-remove/ 本篇收获颇多,特别是关于LinkedList的增删复杂度,之前也没看过LinkedList源码,于是一直以为增删很快。 得到一个结论:任何总结,还是得以源码为基础。所有不看源码的总结都是耍流氓。

    作者回复: 👍🏻

    2020-04-11
    3
  • 看不到de颜色
    老师这期的课程太让人产生共鸣了。之前生产就出过问题。调用方法,达到了用Arrays.asList返回的集合,然后对集合操作时就出了一场。当时看了asList的源码时才发现JDK居然还有这种坑。subList也确实是一个很容易采坑的地方,subList本质上就是把原List报了层皮返回了。关于ListList,头插的话性能应该是会碾压ArrayList,但是就看有没有这种场景了。 课后练习: 1.根据API可以看出,remove(int index) / remove(Object element) 2.Iterator过程中集合结构不能发生变化,通常是遍历过程中其他线程对集合进行了add/remove。可以用CopyOnWrite集合来避免。

    作者回复: 有共鸣就好

    2020-04-02
    3
  • 蚂蚁内推+v
    int[] arr = {1, 2, 3}; List list = Arrays.asList(arr); System.out.println(list + " " + list.size() + " " + list.get(0).getClass()); [1, 2, 3] 3 class java.lang.Integer 为何我本地和老师演示的不一样??

    作者回复: jdk几?

    2020-04-02
    3
    2
  • pedro
    第二个问题,使用 for-each 或者 iterator 进行迭代删除 remove 时,容易导致 next() 检测的 modCount 不等于 expectedModCount 从而引发 ConcurrentModificationException。 在单线程下,推荐使用 next() 得到元素,然后直接调用 remove(),注意是无参的 remove; 多线程情况下还是使用并发容器吧😃

    作者回复: 👍🏻

    2020-03-31
    2
收起评论
显示
设置
留言
47
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部