美文网首页
Android | Java 基础 为什么在遍历的时候List

Android | Java 基础 为什么在遍历的时候List

作者: gc都无法回收的垃圾 | 来源:发表于2020-12-21 10:54 被阅读0次

今天在群里聊天时(摸鱼)看见一个问题,为什么遍历List的时候不能remove?
啥?你在逗我吗?凭什么不能remove,我给你remove一个看看。

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for (int i = 0; i < list.size(); i++) {
            list.remove(i);
        }

run!

Process finished with exit code 0

"for each遍历"
"..."

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for (String s: list){
            list.remove(s);
        }
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at practice.ListTest.main(ListTest.java:14)

遇事不决看源码

原因

众所周知,for each的本质就是Iterator在next()查询元素,将java文件编译后的class文件打开即可看到

        List<String> list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            list.remove(s);
        }

然后查看ArrayList.java:851源码,ArrayList的Iterator 在next()最开始之前进行检查,同样的remove方法也会进行checkForComodification()检查。

  public E next() {
            checkForComodification();
            ...
        }

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

然后打开此方法,当modCount 不等于expectedModCount的时候就会抛出该异常,那么这个modCount 和expectedModCount又是什么呢?

  final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

先看arraylist的add方法

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

然后点进去

  private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

  private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

这里的方法是判断arraylist在添加元素的时候是否需要扩容,在ensureExplicitCapacity方法里找到了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;
        ...
}

我们看到,看arralist的Iterator遍历器的实例变量里,expectedModCount 就等于modCount,也就是说在一开始expectedModCount的数量等于arralist的数量,这也就说明了,在第一次Iterator的next方法里并没有报错,因为modCount = expectedModCount,所以错误只能出在第二次next方法里,然后接下来看arraylist的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;
    }

  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
    }

看到fastRemove方法里的第一行应该就清楚原因了,remove的时候modCount增加了,和一开始的expectedModCount ,也就是arraylist的一开始的数量不一致了,所以会导致ConcurrentModificationException。
所以正确的用法是什么?

正确用法

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator iterator = list.iterator();
        while(iterator.hasNext()) {
            String s = (String)iterator.next();
            //if(...)
            iterator.remove();
        }

why?

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

延伸

那么有没有一种list,能直接就在遍历的时候直接进行删除呢?
答案肯定是有的(听大佬说的)
CopyOnWriteArrayList

不信?试试

        List<String> list = new CopyOnWriteArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for(String s : list){
           list.remove(s);
        }

Process finished with exit code 0

CopyOnWriteArrayList如何做到的?
CopyOnWriteArrayList 类的所有可变操作(add,set等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。写完之后,再将修改完的副本替换成原来的数据,这样就可以保证写操作不会影响读操作了。

从 CopyOnWriteArrayList 的名字可以看出,CopyOnWriteArrayList 是满足 CopyOnWrite 的 ArrayList,所谓 CopyOnWrite 的意思:、就是对一块内存进行修改时,不直接在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,再将原来指向的内存指针指到新的内存,原来的内存就可以被回收。

看看它的add方法

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();//获取当前已有的数组
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝一份新的数组
            newElements[len] = e;//将增加的值放入新的数组
            setArray(newElements);//把当前数组对象设置为刚刚拷贝的数组值
            return true;
        } finally {
            lock.unlock();
        }
    }

remove(object)方法

  public boolean remove(Object o) {
        Object[] snapshot = getArray();
        int index = indexOf(o, snapshot, 0, snapshot.length);//找到当前元素的下标索引值
        return (index < 0) ? false : remove(o, snapshot, index);
    }

    private boolean remove(Object o, Object[] snapshot, int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁
        try {
            Object[] current = getArray();//再次获取当前数组
            int len = current.length;
            if (snapshot != current) findIndex: {//查找需要移除的元素在数组里的索引
                int prefix = Math.min(index, len);
                for (int i = 0; i < prefix; i++) {
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                        index = i;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(o, current, index, len);
                if (index < 0)
                    return false;
            }
            Object[] newElements = new Object[len - 1];//创建一个新的数组,拷贝
            System.arraycopy(current, 0, newElements, 0, index);
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            setArray(newElements);//设置拷贝后的数组
            return true;
        } finally {
            lock.unlock();//释放锁
        }
    }

从add方法和remove方法里不难看出,不管是添加元素还是移除元素,都是通过拷贝数组并重新赋值来实现的,所以在遍历时,remove或者add或者其他一些列操作都不会引起和arraylist一样的异常的,甚至,你在使用Iterator的时候,它还会报错。

private static class COWSubListIterator<E> implements ListIterator<E> {
        ...
        public void remove() {
            throw new UnsupportedOperationException();
        }

        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        public void add(E e) {
            throw new UnsupportedOperationException();
        }
         ...
}


List<String> list = new CopyOnWriteArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String s = (String)var2.next();
            var2.remove();
        }


Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1176)
    at practice.ListTest.main(ListTest.java:19)

总结

1.ArrayList在foreach的时候不能直接使用list.remove来操作数组,因为ArrayList的Iterator 的next方法里每次都会判断当前的数组的数量是否和修改后的数量是否对等,也就是expectedModCount 和modCount,而list.remove方法会修modCount的数量,所以下一次判断时就不对等,就会报错。
2.CopyOnWriteArrayList 可以实现遍历时直接list.remove,因为CopyOnWriteArrayList 的增删是通过每次都拷贝一次数组重新赋值实现的。
3.这个算是java的基础,我居然都不知道。
4.我是不可回收垃圾。

相关文章

网友评论

      本文标题:Android | Java 基础 为什么在遍历的时候List

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