Java集合源码之遍历删除ArrayList元素的坑

先看需求,现有一个ArrayList,泛型是String,且内含有四个元素"a","b","b","c"。

        List list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("b");
        list.add("c");

现需要删除集合中元素为"b"的所有元素。

先来看第一种错误写法:

 String s ;
        for (int i = 0; i 

运行结果:


image.png

结果只删除了第一个b,第二个还好好的待在那里!

WTF??这么简单的逻辑竟然会出现问题?别着急我们打开源码看看,究竟是为什么。直接打开list.remove方法。

 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;
    }

根据我们的情况他应该是直接进到了else里执行循环进行匹配,在匹配结果为true后进入fastRemove方法。我们继续深入。

image.png

首先,modCount++,这个属性记录着List的结构修改次数(增删,改不算)。第二句记录着删除一个元素后,该index的后面的元素共需要挪几位。这里我们要删除第一个"b"元素 索引是 1 ,numMoved = 2,所以是大于0的。然后问题就出现在下一步操作。它执行了System.arraycopy方法。这个方法意思是从源数组的index+1元素开始复制后面的所有元素到目标数组的index位置开始,numMoved个长度。

在我们的场景来看就是这样的:

image.png

然后最后一步,移除末尾元素,末尾的"c"被删除了。

聪明的小伙伴已经发现问题了,移除这一步操作是没有问题的。但是我们的for循环仍在继续,下一次循环i=2,而此时index==2的元素是 "c" !所以equals返回了false,至此执行结束。导致只删除了第一个"b"。

再来看第二种错误写法:

 for(String s:list){
            if(s.equals("b")){
                list.remove(s);
            }
        }

第二次我们使用增强for循环来演示一把。

image.png

运行结果报出了著名的并发修改异常 java.util.ConcurrentModificationException。看到这里有的小伙伴又要懵逼了,我单线程操作集合,怎么还会报出并发修改的异常???

熟悉集合的小伙伴可能知道,增强for循环遍历集合其实是使用了Iterator迭代器的hasNext,next来遍历集合。我们这种写法其实是使用Iterator遍历,但是用了ArrayList的remove方法做删除。下面我们来看看这样会出现怎么样的问题。

image.png

从控制台报错信息我们也不难看出,正是ArrayList中的内部类Itr的checkForComodification方法。进去看看。

截取ArrayList内部类Itr的部分源码:

  private class Itr implements Iterator {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

关于这个Itr的源码我会在其他文章再做解读,我们先关注本次的重点。
请看next方法的第一行代码调用了checkForComodification()。这不就是刚才出问题的代码吗,我们进去看看。

image.png

modcount记录着List结构修改的次数,在上文中已经说过了。明确了这一点,我们再看,Itr在初始化的时候会将modCount赋给expectedCount。(这里其实是用来检测并发错误的,这涉及到多线程的知识。简言之如果在Itr迭代的过程中List被结构修改,那么它的modCount必定会增加。于是到了Itr获取元素的时候就会发现expectedCount与之前的modCunt不一致从而提醒用户并发异常)。因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。

那么怎样才能优雅的删除掉这两个"b"元素呢?

这里提供两种解决方案:

  • 第一种是从后向前遍历ArrayList。因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历。
    for(int i=list.size()-1;i>=0;i--)
    {
        Strings=list.get(i);
        if(s.equals("b"))
        {
            list.remove(s);
        }
    }
  • 第二种是我比较推荐的,显示的使用Iterator遍历并删除该元素。
 Iterator it = list.iterator();
        while (it.hasNext()) 
        {
            String s = it.next();
            if (s.equals("b")) 
            {
                it.remove();
            }
        }

你可能感兴趣的:(Java集合源码之遍历删除ArrayList元素的坑)