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

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

你好,我是朱晔。今天,我来和你说说 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
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《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方法,否则抛异常。

    作者回复: 点赞

    4
    101
  • 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(); } ```

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

    38
  • 👽
    思考题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中也有我的实现

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

    作者回复: 很不错的总结

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

    作者回复: :)

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

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

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

    作者回复: 👍🏻

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

    作者回复: 有共鸣就好

    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几?

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

    作者回复: 👍🏻

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