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
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容