ArrayList在遍历时怎么删除?Arrays.asList中的数据为什么不能修改?

前言

在遍历ArrayList时候进行删除一不小心就会报错,之前遇到直接查询答案用Iterator遍历再删除就可以了,没深究,今天有时间想好好研究下,就准备把List的所有遍历方法都尝试一下,看看哪些会报错,为什么会错,没想到在准备测试数据阶段就载了个跟头

准备数据阶段

看下面的代码,会报错吗?什么错?

   private List list;
   @Test
   public void t10() {
       for (Integer tar : list) {
           if (tar >= 2)
               list.remove(tar);
       }
   }

   @Before
   public void before() {
       Integer[] arr = {0, 1, 2, 3, 4};
       list = Arrays.asList(arr);
   }

我说一个异常,大家肯定熟悉ConcurrentModificationException,这个异常是遍历时不正确删除导致的,具体哪些不正确我下面再说。问题是上面的代码不是报这个错,而是:

java.lang.UnsupportedOperationException
at java.util.AbstractList.remove(AbstractList.java:161)

Arrays.asList的真面目

找到我们上面遇到的异常处,什么鬼?AbstractList不想和你说话,并向你抛出了一个异常:

/**
 * {@inheritDoc}
 *
 * <p>This implementation always throws an
 * {@code UnsupportedOperationException}.
 *
 * @throws UnsupportedOperationException {@inheritDoc}
 * @throws IndexOutOfBoundsException     {@inheritDoc}
 */
public E remove(int index) {
    throw new UnsupportedOperationException();
}

确实什么事都没做直接抛,注释也说了这个实现总会抛出异常,不只这一个方法,像add(int index, E element), remove(int index)也是直接抛的,这是个抽象类嘛,实现的一部分可以共用,另一部分要子类具体实现的,说明Attars.asList返回的List并没有实现这个方法,进到Arrays里面:

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

哇,是ArrayList,真的是我们熟悉的那个ArraysList吗,不,根本没有这个构造,它的真面目是:

private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    private static final long serialVersionUID = -2764017481108945198L;
    private final E[] a;

    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }
    
    。。。。。。
}

只是Arrays中的一个静态内部类,Objects.requireNonNull是一个是否为空的判断,不用管它。对于这个ArrayList,没有实现add(E e), remove(int index)remove(Object o)等方法,自然是不可修改的。它的用处也不算少见,要想操作数据可以:

    List<Integer> list = new ArrayList<>();
    list.addAll(Arrays.asList(arr));

ArrayList遍历删除

错误1

由于foreach语法比fori简单好用,大多数场合我们是能用就用,懒就要有懒的代价啊,要是用fori,你怎么删除都不会报错,但删除后可能并不是你想要的效果哟!详见错误3。回到正题,foreach其实是用Iterator来遍历的,对于集合来说,只要实现了java.lang.Iterable接口,就可以使用foreach
看下面代码,有什么错误?

    Integer[] arr = {0, 1, 2, 3, 4};
    List<Integer> list = new ArrayList<>();
    list.addAll(Arrays.asList(arr));
    for (Integer tar : list) {
        if (tar == 2){
            list.remove(tar);
        }
    }

答案:报错ConcurrentModificationException
位置在下面代码的倒数第三行,下面是 ArrayList中的迭代器。当我们使用foreach时,就是使用这个迭代器工作的,cursor是游标,指示当前已取出元素的下一个元素,lastRet指示当前已取出元素,expectedModCount是期待的修改次数,modCount是实际修改次数,每次循环都会先调用hasNext(),当游标不等于(即小于)list.size()时说明还有下一个元素,再调用next取出下一个值,next()方法的第一个方法就是checkForComodification(),检查期待的修改次数是否与实际相等,不相等就抛异常,expectedModCount变量范围是这个迭代器,使用list.remove(Object obj)只会使modCount++,expectedModCount的值不变自然就出错了。所以采用Iterator遍历是个明智的选择,它的remove()方法里面ArrayList.this.remove(lastRet)会 让modCount++,但随后又把modCount的值赋给了expectedModCount,继续循环不会出问题。

    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;

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

    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();
    }
}
小提问:把上面的遍历tar == 2改成tar == 3还会错吗?

如果你把上面的解释仔细看了的话,想必已经知道答案了,不会。
解答:当tar == 3时,当前游标cursor是4,size是5,但当删除了tarsize就变成了4,和cursor相等了,到下一次循环,hasNext()判断时为false,所以结束了循环,不给它抛异常的机会。

错误2

你以为用了Iterator遍历就高枕无忧了吗?下面代码正确吗?

    Integer[] arr = {0, 1, 2, 3, 4};
    List<Integer> list = new ArrayList<>();
    list.addAll(Arrays.asList(arr));
    for (Iterator<Integer> iter = list.iterator(); iter.hasNext(); ) {
        Integer tar = iter.next();
        if (tar == 2)
            list.remove(tar);
    }

依然微笑的给你抛个异常,虽然用的是Iterator遍历,但是删除方法依然是ArrayList自己的,不是iter.remove(),这个错误和错误1是一样的,属于脱裤子放屁。

错误3

这个是特殊的,不报错,但是你处理完成后的数据很可能不是你想要的。
猜猜看剩下什么?

    Integer[] arr = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
    List<Integer> list = new ArrayList<>();
    list.addAll(Arrays.asList(arr));
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i) > 3)
            list.remove(i);
    }

答案:[0,1,2,3,5,7,9,11]
解析:这一切都是字母i的锅,删除4之前,i = 4list.get(i) = 4, 删除4之后i不变,但是list.get(i) = 5, 因为后面的元素都向前进了一步,所以每删除一个,这个被删除元素的下一个就幸免于难了。
解决方法:list.remove(i--), 真是机智又优雅, 开心,其实我最喜欢的遍历方法就是这个。

总结

上面的错误方法都附了解答,有些小伙伴不喜欢看步骤,就要答案,那么总结下ArrayList的正确遍历方法

Integer[] arr = {0, 1, 2, 3, 4, 5, 6, 7};
List<Integer> list = new ArrayList<>();
list.addAll(Arrays.asList(arr));
//第一种
for (Iterator<Integer> iter = list.iterator(); iter.hasNext(); ) {
    Integer tar = iter.next();
    if (tar == 2)
        iter.remove();
}
//第二种
for (int i = 0; i < list.size(); i++) {
    if (list.get(i) > 3)
        list.remove(i--);
} 

上面只是针对在遍历时需要多次删除时使用,如果你不删除,或者只删除一次然后break结束循环,你随便用任何你喜欢的方式。
还有remove(int index)remove(Object o)两个方法,你可以认为是一样的,remove(Object o)无非是内部遍历查找equalstrue的元素或者都为null的元素,然后调用fastRemove(int index)删除,干说无力,还是贴上代码

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

private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}


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

推荐阅读更多精彩内容