欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

List如何一边遍历,一边删除

程序员文章站 2022-04-11 18:59:49
...

1、foreach抛出ConcurrentModificationException

        List<String> strList = new ArrayList<>();
        strList.add("Java");
        strList.add("Docker");
        strList.add("SpringCloud");
        for (String str : strList) {
            if ("Java".equals(str)) {
                strList.remove(str);
            }
        }
        System.out.println(strList);

报错信息如下:

List如何一边遍历,一边删除

List如何一边遍历,一边删除

通过其生成的字节码可以看出,foreach循环在实际执行时使用的是Iterator,使用的核心方法是hasnext()next()

ArrayList类中Iterator的实现如下:

    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
      	//初始化的时候expectedModCount等于modCount
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
          	//调用checkForComodification,校验modCount是否等于expectedModCount,如果不等于抛出异常ConcurrentModificationException
            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();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

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

调用next()方法获取下一个元素时,调用了checkForComodification()校验modCount是否等于expectedModCount,如果不等于抛出异常ConcurrentModificationException

在上面的案例中,刚开始modCountexpectedModCount的值都为3,所以第1次获取元素是没问题的,但是当执行完strList.remove(str)modCount的值就被修改成了4

    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
    }

所以在第2次获取元素时,modCountexpectedModCount的值就不相等了,所以抛出了异常ConcurrentModificationException

那么List一边遍历一边删除正确的实现方法有哪些呢

1)、使用Iteratorremove()方法

2)、JDK1.8新增的removeIf()方法

3)、使用for循环正序遍历

4)、使用for循环倒序遍历

2、使用Iteratorremove()方法

        List<String> strList = new ArrayList<>();
        strList.add("Java");
        strList.add("Docker");
        strList.add("SpringCloud");
        Iterator<String> iterator = strList.iterator();
        while (iterator.hasNext()) {
            if ("Java".equals(iterator.next())) {
                iterator.remove();
            }
        }
        System.out.println(strList);

ArrayList中的Itr实现源码如下:

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
              	//expectedModCount、modCount这2个变量的值又相等了
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

每次删除一个元素,都会将modCount的值重新赋值给expectedModCount,这样2个变量就相等了

3、JDK1.8新增的removeIf()方法

        List<String> strList = new ArrayList<>();
        strList.add("Java");
        strList.add("Docker");
        strList.add("SpringCloud");
        strList.removeIf(str -> "Java".equals(str));
        System.out.println(strList);

或者

        List<String> strList = new ArrayList<>();
        strList.add("Java");
        strList.add("Docker");
        strList.add("SpringCloud");
        strList.removeIf("Java"::equals);
        System.out.println(strList);

Collection接口中removeIf()方法默认实现采用的也是Iteratorremove()方法

    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

4、使用for循环正序遍历

        List<String> strList = new ArrayList<>();
        strList.add("Java");
        strList.add("Docker");
        strList.add("SpringCloud");
        for (int i = 0; i < strList.size(); ++i) {
            String str = strList.get(i);
            if ("Java".equals(str)) {
                strList.remove(i--);
            }
        }
        System.out.println(strList);

正序遍历需要修正下标,因为刚开始元素是:

0 Java
1 Docker
2 SpringCloud

第一次循环将Java删除后,元素的下标变成下面这样:

0 Docker
1 SpringCloud

如果不修正下标,第二次循环时i的值是1,也就是取到了SpringCloud,这样就导致元素Docker被跳过检查了

5、使用for循环倒序遍历

        List<String> strList = new ArrayList<>();
        strList.add("Java");
        strList.add("Docker");
        strList.add("SpringCloud");
        for (int i = strList.size() - 1; i >= 0; --i) {
            String str = strList.get(i);
            if ("SpringCloud".equals(str)) {
                strList.remove(i);
            }
        }
        System.out.println(strList);

倒序遍历不需要修正下标,因为刚开始元素是:

0 Java
1 Docker
2 SpringCloud

第一次循环将SpringCloud删除后,元素的下标变成下面这样:

0 Java
1 Docker

第二次循环时i的值是1,也就是取到了元素Docker,不会导致跳过元素,所以不需要修正下标

参考:

https://blog.csdn.net/zwwhnly/article/details/104987143