美文网首页
ArrayList在遍历时怎么删除?Arrays.asList中

ArrayList在遍历时怎么删除?Arrays.asList中

作者: 天天读三国 | 来源:发表于2018-03-07 22:55 被阅读0次

    前言

    在遍历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
    }
    

    相关文章

      网友评论

          本文标题:ArrayList在遍历时怎么删除?Arrays.asList中

          本文链接:https://www.haomeiwen.com/subject/mousfftx.html