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

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

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

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

先来看第一种错误写法:

 String s ;
        for (int i = 0; i <list.size() ; i++) {
            s = list.get(i);
            if("b".equals(s)){
                list.remove(s);
            }
        }
        System.out.println(list.toString());

运行结果:


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<E> {
        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<String> it = list.iterator();
        while (it.hasNext()) 
        {
            String s = it.next();
            if (s.equals("b")) 
            {
                it.remove();
            }
        }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容