首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >在ArrayList的循环中删除元素,会不会出现问题?

在ArrayList的循环中删除元素,会不会出现问题?

作者头像
Wizey
发布于 2018-09-29 01:48:42
发布于 2018-09-29 01:48:42
3.3K02
代码可运行
举报
文章被收录于专栏:编程心路编程心路
运行总次数:2
代码可运行

在 ArrayList 的循环中删除元素,会不会出现问题?我开始觉得应该会有什么问题吧,但是不知道问题会在哪里。在经历了一番测试和查阅之后,发现这个“小”问题并不简单!

不在循环中的删除,是没有问题的,否则这个方法也没有存在的必要了嘛,我们这里讨论的是在循环中的删除,而对 ArrayList 的循环方法也是有多种的,这里定义一个类方法 remove(),先来看段代码吧。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("aa");
        list.add("bb");
        list.add("aa");
        list.add("bb");
        list.add("cc");
        // 删除元素 aa
        remove(list, "aa");
        for (String str : list) {
            System.out.println(str);
        }
    }
    public static void remove(ArrayList<String> list, String elem) {
        // 不同的循环及删除方法
        // 方法一:普通for循环正序删除,删除过程中元素向左移动,不能删除重复的元素
//        for (int i = 0; i < list.size(); i++) {
//            if (list.get(i).equals("bb")) {
//                list.remove(list.get(i));
//            }
//        }
        // 方法二:普通for循环倒序删除,删除过程中元素向左移动,可以删除重复的元素
//        for (int i = list.size() - 1; i >= 0; i--) {
//            if (list.get(i).equals("bb")) {
//                list.remove(list.get(i));
//            }
//        }
        // 方法三:增强for循环删除,使用ArrayList的remove()方法删除,产生并发修改异常 ConcurrentModificationException
//        for (String str : list) {
//            if (str.equals("aa")) {
//                list.remove(str);
//            }
//        }

        // 方法四:迭代器,使用ArrayList的remove()方法删除,产生并发修改异常 ConcurrentModificationException
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()) {
//            if(iterator.next().equals(elem)) {
//                list.remove(iterator.next());
//            }
//        }

        // 方法五:迭代器,使用迭代器的remove()方法删除,可以删除重复的元素,但不推荐
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()) {
//            if(iterator.next().equals(elem)) {
//                iterator.remove();
//            }
//        }
    }
}

这里我测试了五种不同的删除方法,一种是普通的 for 循环,一种是增强的 foreach 循环,还有一种是使用迭代器循环,一共这三种循环方式。也欢迎你点击文末的 “阅读全文”,留言和我们讨论哦!

上面这几种删除方式呢,在删除 list 中单个的元素,也即是没有重复的元素,如 “cc”。在方法三和方法四中都会产生并发修改异常 ConcurrentModificationException,这两个删除方式中都用到了 ArrayList 中的 remove() 方法(快去上面看看代码吧)。而在删除 list 中重复的元素时,会有这么两种情况,一种是这两个重复元素是紧挨着的,如 “bb”,另一种是这两个重复元素没有紧挨着,如 “aa”。删除这种元素时,方法一在删除重复但不连续的元素时是正常的,但在删除重复且连续的元素时,会出现删除不完全的问题,这种删除方式也是用到了 ArrayList 中的 remove() 方法。而另外两种方法都是可以正常删除的,但是不推荐第五种方式,这个后面再说。

经过对运行结果的分析,发现问题都指向了 ArrayList 中的 remove() 方法,(感觉有种侦探办案的味道,可能是代码写多了的错觉吧,txtx...)那么看 ArrayList 源码是最好的选择了,下面是我截取的关键代码(Java1.8)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

可以看到这个 remove() 方法被重载了,一种是根据下标删除,一种是根据元素删除,这也都很好理解。

根据下标删除的 remove() 方法,大致的步骤如下:

  • 1、检查有没有下标越界,就是检查一下当前的下标有没有大于等于数组的长度
  • 2、列表被修改(add和remove操作)的次数加1
  • 3、保存要删除的值
  • 4、计算移动的元素数量
  • 5、删除位置后面的元素向左移动,这里是用数组拷贝实现的
  • 6、将最后一个位置引用设为 null,使垃圾回收器回收这块内存
  • 7、返回删除元素的值

根据元素删除的 remove() 方法,大致的步骤如下:

  • 1、元素值分为null和非null值
  • 2、循环遍历判等
  • 3、调用 fastRemove(i) 函数
    • 3.1、修改次数加1
    • 3.2、计算移动的元素数量
    • 3.3、数组拷贝实现元素向左移动
    • 3.4、将最后一个位置引用设为 null
    • 3.5、返回 fase
  • 4、返回 true

这里我有个疑问,第一个 remove() 方法中的代码和 fastRemove() 方法中的代码是完全一样的,第一个 remove() 方法完全可以向第二个 remove() 方法一样调用 fastRemove() 方法嘛,这里代码感觉有些冗余,个人理解有限,还请知道的大佬指教。

我们重点关注的是删除过程,学过数据结构的小伙伴可能手写过这样的删除,下面我画个图来让大家更清楚的看到整个删除的过程。以删除 “bb” 为例,当指到下标为 1 的元素时,发现是 "bb",此处元素应该被删除,根据上面的删除步骤可知,删除位置后面的元素要向前移动,移动之后 “bb” 后面的 “bb” 元素下标为1,后面的元素下标也依次减1,这是在 i = 1 时循环的操作。在下一次循环中 i = 2,第二个 “bb” 元素就被遗漏了,所以这种删除方法在删除连续重复元素时会有问题。

循环中的正序删除.jpg

但是如果我们使 i 递减循环,也即是方法二的倒序循环,这个问题就不存在了,如下图。

循环中的倒序删除.jpg

既然我们已经搞清不能正常删除的原因,那么再来看看方法五中可以正常删除的原因。方法五中使用的是迭代器中的 remove() 方法,通过阅读 ArrayList 的源码可以发现,有两个私有内部类,Itr 和 ListItr,Itr 实现自 Iterator 接口,ListItr 继承 Itr 类和实现自 ListIterator 接口。Itr 类中也有一个 remove() 方法,迭代器实际调用的也正是这个 remove() 方法,我也截取这个方法的源码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private class Itr implements Iterator<E>
private class ListItr extends Itr implements ListIterator<E> 
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification(); // 检查修改次数

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

可以看到这个 remove() 方法中调用了 ArrayList 中的 remove() 方法,那为什么方法四会抛出并发修改异常而这里就没有问题呢?这里注意 expectedModCount 变量和 modCount 变量,modCount 在前面的代码中也见到了,它记录了 list 修改的次数,而前面还有一个 expectedModCount,这个变量的初值和 modCount 相等。在 ArrayList.this.remove(lastRet); 代码前面,还调用了检查修改次数的方法 checkForComodification(),这个方法里面做的事情很简单,如果 modCount 和 expectedModCount 不相等,那么就抛出 ConcurrentModificationException,而在这个 remove() 方法中存在 ``expectedModCount = modCount`,两个变量值在 ArrayList 的 remove() 方法后,进行了同步,所以不会有异常抛出,并且在循环过程中,也不会遗漏连续重复的元素,所以可以正常删除。上面这些代码都是在单线程中执行的,如果换到多线程中,方法五不能保证两个变量修改的一致性,结果具有不确定性,所以不推荐这种方法。而方法一在单线程和多线程中都是可以正常删除的,多线程中测试代码如下,这里我只模拟了三个线程:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import java.util.ArrayList;
import java.util.Iterator;

public class MultiThreadArrayList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("aa");
        list.add("bb");
        list.add("bb");
        list.add("aa");
        list.add("cc");
        list.add("dd");
        list.add("dd");
        list.add("cc");
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                remove(list,"aa");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                remove(list, "bb");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread3 = new Thread() {
            @Override
            public void run() {
                remove(list, "dd");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        // 使各个线程处于就绪状态
        thread1.start();
        thread2.start();
        thread3.start();
        // 等待前面几个线程完成
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (String str : list) {
            System.out.println(str);
        }
    }

    public static void remove(ArrayList<String> list, String elem) {
        // 普通for循环倒序删除,删除过程中元素向左移动,不影响连续删除
        for (int i = list.size() - 1; i >= 0; i--) {
            if (list.get(i).equals(elem)) {
                list.remove(list.get(i));
            }
        }

        // 迭代器删除,多线程环境下无法使用
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()) {
//            if(iterator.next().equals(elem)) {
//                iterator.remove();
//            }
//        }
    }
}

既然 Java 的循环删除有问题,发散一下思维,Python 中的列表删除会不会也有这样的问题呢,我抱着好奇试了试,发现下面的方法一也同样存在不能删除连续重复元素的问题,方法二则是报列表下标越界的异常,测试代码如下,这里我只测试了单线程环境:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
list = []
list.append("aa")
list.append("bb")
list.append("bb")
list.append("aa")
list.append("cc")
# 方法一
# for str in list:
#     if str == "bb":
#         list.remove(str)
# 方法二
for i in range(len(list)):
    if list[i] == "bb":
        list.remove(list[i])
for str in list:
    print(str)

总结:一道看似很简单的问题,没想到背后却有这么多的知识,真是感觉自己要学的还很多,遇到方法细节的问题,我觉得直接看源码是最好的解决方法,另外我觉得在后面的版本的 JDK 中,可以增加一个在循环中删除连续元素的方法嘛,不然这里对于没有发现这个问题的人真是个坑。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.09.07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【Java】ConcurrentModificationException梳理
首先看一下一个例子举出 ConcurrentModificationException 的出现场景:
后端码匠
2023/09/02
2090
【Java】ConcurrentModificationException梳理
JDK1.8源码(五)——java.util.ArrayList 类
  关于 JDK 的集合类的整体介绍可以看这张图,本篇博客我们不系统的介绍整个集合的构造,重点是介绍 ArrayList 类是如何实现的。 1、ArrayList 定义 ArrayList 是一个用
IT可乐
2018/03/30
1.1K0
JDK1.8源码(五)——java.util.ArrayList 类
ArrayList的删除姿势你都知道了吗
前几天有个读者由于看了《ArrayList哪种遍历效率最好,你真的弄明白了吗?》问了个问题普通for循环ArrayList为什么不能删除连续重复的两个元素?其实这个描述是不正确的。正确的应该是普通for循环正序删除,不能删除连续的元素所以就产生了这个文章。
java金融
2020/06/23
8700
ArrayList的删除姿势你都知道了吗
ArrayList 源码分析
在 Java 中当创建数组时会在内存中划分一块连续的内存,然后当有数据进入的时候会将数据按顺序的存储在这块连续的内存中。当需要读取数组中的数据时,需要提供数组中的索引,然后数组根据索引将内存中的数据取出来,返回给读取程序。在 Java 中并不是所有的数据都能存储到数组中,只有相同类型的数据才可以一起存储到数组中。
希希里之海
2019/09/05
4820
一道面试题引发的思考
为什么呢?那么我们怎么来发现它背后的秘密呢?答案只有一个:那就是通过源码来解惑(ArrayList部分源码)。
阿豪聊干货
2018/08/09
2050
List中remove()方法的陷阱,被坑惨了!
Java的List在删除元素时,一般会用list.remove(o)/remove(i)方法。在使用时,容易触碰陷阱,得到意想不到的结果。总结以往经验,记录下来与大家分享。
好好学java
2021/10/09
6740
(五)Java并发学习笔记--线程安全-同步容器
在Java的集合容器框架中,主要有四大类别:List、Set、Queue、Map。
用户1212940
2022/04/13
2880
(五)Java并发学习笔记--线程安全-同步容器
并发修改异常ConcurrentModificationException详解
在多线程编程中,相信很多小伙伴都遇到过并发修改异常ConcurrentModificationException,本篇文章我们就来讲解并发修改异常的现象以及分析一下它是如何产生的。
终有救赎
2023/10/22
8640
并发修改异常ConcurrentModificationException详解
ArrayList源码完全分析
本文介绍了Java中的ArrayList类,它是Java编程语言中最常用的集合类之一。ArrayList用于存储一系列元素,允许添加、删除和随机访问元素。ArrayList的底层实现是数组,它通过ArrayList的实例变量来存储元素。在ArrayList中,添加、删除和随机访问元素的时间复杂度分别是O(1)、O(1)和O(1)。ArrayList的常用方法有add、remove、set、get、size、iterator、listIterator等。同时,文章还对ArrayList的性能、容量预估、扩容、元素访问顺序等方面进行了详细阐述。
MelonTeam
2018/01/04
9900
ArrayList与CopyOnWriteArrayList常见操作与问题
在工作和学习中,经常碰到删除ArrayList里面的某个元素,看似一个很简单的问题,却很容易出bug。不妨把这个问题当做一道面试题目,我想一定能难道不少的人。今天就给大家说一下在ArrayList循环遍历并删除元素的问题。首先请看下面的例子:
chengcheng222e
2021/11/04
8370
Java面试题之List(一)
我们都知道,Java面试中避免不了的要涉及到对各种框架和源码的盘问。有的面试官会直接点,问我们对ArrayList的源码的理解,有的会间接的提问。
黑洞代码
2021/01/14
4510
Java集合遍历时遇到的坑
面试题中可能会被问到对Java集合的了解情况,并深入集合底层的源码,以及使用集合的时候有没有遇到坑——这时候其实是想考察大家在日常工作中是否细心,一般不建议说,没有遇到坑,因为这样会给面试官感觉大家在工作中不细心,不仔细。
黑洞代码
2021/01/14
6490
Java集合遍历时遇到的坑
已解决Java中java.util.ConcurrentModificationException异常
成功解决Java ConcurrentModificationException异常
程序员洲洲
2024/06/07
1560
ArrayList的删除姿势你都掌握了吗
前几天有个读者由于看了《ArrayList哪种遍历效率最好,你真的弄明白了吗?》问了个问题普通for循环ArrayList为什么不能删除连续重复的两个元素?其实这个描述是不正确的。正确的应该是普通for循环正序删除,不能删除连续的元素所以就产生了这个文章。
java金融
2020/08/05
5870
ArrayList的删除姿势你都掌握了吗
Java,你告诉我 fail-fast 是什么鬼?
说起来真特么惭愧:十年 IT 老兵,Java 菜鸟一枚。今天我才了解到 Java 还有 fail-fast 一说。不得不感慨啊,学习真的是没有止境。只要肯学,就会有巨多巨多别人眼中的“旧”知识涌现出来,并且在我这全是新的。
沉默王二
2019/10/31
5630
遍历数据时arraylist效率高于linkedlist_遍历问题种类
一个 java 程序猿比较广为人知的小知识 ,是 ArrayList 和 LinkedList 最好使用迭代器删除,而不是遍历删除。
全栈程序员站长
2022/09/23
7520
遍历数据时arraylist效率高于linkedlist_遍历问题种类
还在自己写迭代器进行remove?快来看看新方法
我们都知道 List 中是不允许在循环的过程中去进行移除元素的,为什么呢?一般的新人可能会遇到这个问题,比如说会从 List 的遍历的过程中去进行 remove 数据,但是干过几年的开发的有经验的工作人员,是肯定不会这么干的,很简单,会报错。
Java极客技术
2022/12/04
2650
还在自己写迭代器进行remove?快来看看新方法
ArrayList详解
ArrayList 是一个数组列表。它的主要底层实现是Object数组,但与 Java 中的数组相比,它的容量能动态变化,可看作是一个动态数组结构。特别注意的是,当我们装载的是基本类型的数据 int,long,boolean,short,byte… 的时候,我们只能存储他们对应的包装类。
程序员阿杜
2023/08/25
3100
arraylist的基本方法_什么是arraylist
Java基础到Java实战全套学习视频教程,包括多个企业级实战项目:https://urlify.cn/YFzABz 密码: pi95 面试算法资料,这是总结的算法资料,学完基本可以应付80%大厂:https://urlify.cn/N7vIj2 密码: ijoi 大厂面试资料,一年时间总结,覆盖Java所有技术点:https://urlify.cn/Vzmeqy 密码: j9t2 面试思维导图,手打总结: https://urlify.cn/vUNF7z 密码: adbo
全栈程序员站长
2022/11/09
8960
arraylist的基本方法_什么是arraylist
ArrayList分析3 : 删除元素
转载请注明出处:https://www.cnblogs.com/funnyzpc/p/16421743.html
上帝
2022/07/09
3230
相关推荐
【Java】ConcurrentModificationException梳理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验