Java 业务开发常见错误 100 例
朱晔
贝壳金服资深架构师
立即订阅
4513 人已学习
课程目录
已更新 15 讲 / 共 37 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 业务代码真的会有这么多坑?
免费
代码篇 (12讲)
01 | 使用了并发工具类库,线程安全就高枕无忧了吗?
02 | 代码加锁:不要让“锁”事成为烦心事
03 | 线程池:业务代码最常用也最容易犯错的组件
04 | 连接池:别让连接池帮了倒忙
05 | HTTP调用:你考虑到超时、重试、并发了吗?
06 | 20%的业务代码的Spring声明式事务,可能都没处理正确
07 | 数据库索引:索引并不是万能药
08 | 判等问题:程序里如何确定你就是你?
09 | 数值计算:注意精度、舍入和溢出问题
10 | 集合类:坑满地的List列表操作
11 | 空值处理:分不清楚的null和恼人的空指针
12 | 异常处理:别让自己在出问题的时候变为瞎子
不定期加餐 (2讲)
加餐1 | 带你吃透课程中Java 8的那些重要知识点(上)
加餐2 | 带你吃透课程中Java 8的那些重要知识点(下)
Java 业务开发常见错误 100 例
登录|注册

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

朱晔 2020-03-31
你好,我是朱晔。今天,我来和你说说 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/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java 业务开发常见错误 100 例》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(19)

  • 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
    7
  • 👽
    思考题1:
    不一样,
    remove 的两实现,
    包装类型调用的是 boolean remove(Object o);方法,本质上是寻找集合中是否有该元素,有则删除。
    基本类型int 调用的是public E remove(int index)方法,实现是直接删除下标。
    另外返回值也有区别,
    包装类型remove返回布尔,有该对象则返回true并删除,没有则返回false
    基本类型的remove返回泛型对象,有则返回该对象,因为是跟据下标删除,所以不存在没有的情况,除非下标越界
    2020-03-31
    4
  • 秋水
    真的是迷信了LinkedList
    2020-03-31
    2
  • 失火的夏天
    1.remove包装类数字是删除对象,基本类型的int数字是删除下标。
    2.好像是modcount和什么东西对不上来着,具体忘记了,看看其他大佬怎么说。解决这玩意就是改用迭代器遍历,调用迭代器的remove方法。

    话说到这个linkedlist,真是感觉全面被arraylist压制。那这数据结构还留着干嘛呢?为什么不删掉算了。。。我个人感觉linekdlist只有在头尾加入删除元素的时候有一点点优势了吧。用队列或者双端队列的时候会偶然用到。但是感觉用对应的数组模式实现,效率会更高些,就是要考虑扩容的问题。

    老师能帮忙解答一下linkedlist留下没删是因为什么吗?

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

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

    作者回复: 很不错的总结

    2020-03-31
    1
  • hanazawakana
    第一个问题,remove int是删除下标元素,删除Integer是删除某个元素
    第二个问题,因为迭代器为了防止在遍历时删除插入的操作导致漏遍历到某个元素,所以禁止在遍历时插入和删除元素,解决方法想到的是copy on write。
    2020-04-04
  • 汝林外史
    1. int会调用 public E remove(int index)方法 Integer会调用public boolean remove(Object o)方法

    2.modcount改变导致的异常 改用foreach的方式。
    2020-04-03
  • 看不到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
  • 小美
    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
    1
  • Monday
    Arrays.asList 返回的 List 不支持增删操作。 这个坑一直都知道,只是没去看源码,今天学到了,原来是Arrays的内部类ArrayList没有重写add方法,而是extends AbstractList的add(),后者会抛出UnsupportedOperationException
    2020-04-01
  • 终结者999号
    在转成stream的时候,linkedlist是不是要好于ArrayList呢?
    2020-04-01
  • csyangchsh
    ArrayList分配的内存空间是连续的,对会CPU Cache很友好。LinkedList还要包装成Node,又增加了开销。这个测试使用JMH,根据CPU Cache大小,定义不同的元素个数,可能更严谨一点。

    作者回复: 是的,JMH做微基准测试更严谨一些

    2020-03-31
  • mgs2002
    第二题可以使用并发容器CopyOnWriteArrayList解决,删除和添加都是在快照上面的,不会影响原有的List
    2020-03-31
  • Wiggle Wiggle
    这么看来 LinkedList 没有什么优势,随机插入败了,随机删除也差不多,只剩尾插了。尾插其实也没有多少优势,最多就是arrayList底层满了以后需要扩容,linkedList 不需要。估算下来,大概只有在插入只是海量尾插、查询只是遍历的情况下才有点优势
    2020-03-31
  • pedro
    第二个问题,使用 for-each 或者 iterator 进行迭代删除 remove 时,容易导致 next() 检测的 modCount 不等于 expectedModCount 从而引发 ConcurrentModificationException。
    在单线程下,推荐使用 next() 得到元素,然后直接调用 remove(),注意是无参的 remove; 多线程情况下还是使用并发容器吧😃

    作者回复: 👍🏻

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

    作者回复: :)

    2020-03-31
  • 梦倚栏杆
    1. 刚好是看到案例时我想问的问题,答案知道,但是为什么呢?是规定为了区分?那为什么创建数组时可以自动装箱呢?
    2.用迭代器可以,但是为什么其实也不能说出个所以然


    我没想到的是linklist的性能问题,sublist也没想到,是不是这种很多返回的都是视图呀

    作者回复: java 1.2的时候List接口已经是这样了,泛型后面出来的

    2020-03-31
收起评论
19
返回
顶部