遍历list时删除元素发生了什么?

简介: 遍历list时删除元素发生了什么?

导语:

最近写了一个bug就是在遍历list的时候删除了里面一个元素,其实之前看过阿里的java开发规范,知道在遍历的时候删除元素会产生问题,但是写的快的时候还是会没注意到,那正好研究下里面的机制看。我们看看阿里规范怎么写的:

首先提出一个概念:fail-fast
摘自百度百科:
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
简单来说是java为了防止出现并发异常的一个机制,但是其实在单线程下也可以产生。

实例分析:

接下来,我会通过6个例子来探究下,遍历list时删除元素会发生什么,和它的源码探究。
前提先造了一个list:

private List<String> list = new ArrayList<String>() {{
       add("元素1");
       add("元素2");
       add("元素3");
   }};

1.普通的for循环

public void test1() {
       for (int i = 0; i < list.size() - 1; i++) {
           if ("元素3".equals(list.get(i))) {
               System.out.println("找到元素3了");
           }

           if ("元素2".equals(list.get(i))) {
               list.remove(i);
           }
       }
   }
   //  这里不会输出找到元素3 因为遍历到元素2的时候删除了元素2 list的size变小了
   //  所以就产生问题了

2.for循环另一种情况

public void test2() {
       for (int i = 0; i < list.size() - 1; i++) {
           if ("元素2".equals(list.get(i))) {
               list.remove(i);
           }

           if ("元素3".equals(list.get(i))) {
               System.out.println("找到元素3了");
           }
       }
   }
   // 这里会输出元素3 但是其实是在遍历到元素2的时候输出的
   // 遍历到元素2 然后删除 到了判断元素3的条件的时候i是比原来小了1
   // 所以又阴差阳错的输出了正确结果

3.增强for循环

public void test3() {
       for (String item : list) {
           if ("元素2".equals(item)) {
               list.remove(item);
           }

           if ("元素3".equals(item)) {
               System.out.println("找到元素3了");
           }
       }
   }
   // 这里和上面的结果有点不一样 但是还是没有输出元素3的打印语句
   // 这里反编译下java文件就可以知道是为啥啦

   public void test3() {
         Iterator var1 = this.list.iterator();

         while(var1.hasNext()) {
           // 为了显示区别这里
             String var2 = (String)var1.next();
             if ("元素2".equals(var2)) {
                 this.list.remove(var2);
             }

             if ("元素3".equals(var2)) {
                 System.out.println("找到元素3了");
             }
         }

     }

反编译的文件可以知道这里增强for反编译是用了迭代器,来判断是否还有元素,然后remove用的是list的方法。
我们看下ArrayList中的hasNext(),next()是怎么写的。下面是ArrayList中的内部类Itr实现了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
        int expectedModCount = modCount;

        Itr() {}

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

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

这里cursor在next()执行时,cursor = i + 1,所以当运行到"元素2".equal(item)这里,元素2被移除,当遍历当元素3的时候size = 2.cursor = i + 1 = 1 + 1 也是2.
hasNext()中cursor == size就直接退出了。

4.foreach

public void test4() {
        list.forEach(
                item -> {
                    if ("元素2".equals(item)) {
                        list.remove(item);
                    }

                    if ("元素3".equals(item)) {
                        System.out.println("找到元素3了");
                    }
                }
        );
    }
    // 这里抛出了我们期待已经的fail-fast java.util.ConcurrentModificationException

点进去看下ArrayList的源码

@Override
   public void forEach(Consumer<? super E> action) {
       Objects.requireNonNull(action);
       final int expectedModCount = modCount;
       @SuppressWarnings("unchecked")
       final E[] elementData = (E[]) this.elementData;
       final int size = this.size;
       for (int i=0; modCount == expectedModCount && i < size; i++) {
           action.accept(elementData[i]);
       }
       if (modCount != expectedModCount) {
           throw new ConcurrentModificationException();
       }
   }

根据报错信息我们可以知道是
if (modCount != expectedModCount) {

throw new ConcurrentModificationException();

}
抛出的异常,那么这个modCount元素是从哪里来的?
我们找到AbstractList,这个是ArrayList的父类。
看看源码里面是怎么说的

/**
    * The number of times this list has been <i>structurally modified</i>.
    * Structural modifications are those that change the size of the
    * list, or otherwise perturb it in such a fashion that iterations in
    * progress may yield incorrect results.
    *
    * <p>This field is used by the iterator and list iterator implementation
    * returned by the {@code iterator} and {@code listIterator} methods.
    * If the value of this field changes unexpectedly, the iterator (or list
    * iterator) will throw a {@code ConcurrentModificationException} in
    * response to the {@code next}, {@code remove}, {@code previous},
    * {@code set} or {@code add} operations.  This provides
    * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
    * the face of concurrent modification during iteration.
    *
    * <p><b>Use of this field by subclasses is optional.</b> If a subclass
    * wishes to provide fail-fast iterators (and list iterators), then it
    * merely has to increment this field in its {@code add(int, E)} and
    * {@code remove(int)} methods (and any other methods that it overrides
    * that result in structural modifications to the list).  A single call to
    * {@code add(int, E)} or {@code remove(int)} must add no more than
    * one to this field, or the iterators (and list iterators) will throw
    * bogus {@code ConcurrentModificationExceptions}.  If an implementation
    * does not wish to provide fail-fast iterators, this field may be
    * ignored.
    */
protected transient int modCount = 0;

英文好的同学可以自己看下,英文不好的同学可以默默打开谷歌翻译或者有道词典了。

剩下的同学可以听一下我的理解,其实只看第一段基本上就知道它是干嘛的了。意思是这个字段是记录了list的结构性修改次数(我们可以理解为add,remove这些会改变list大小的操作)。如果是其他方式导致的就回抛出ConcurrentModificationException异常。
所以我们再去看看子类的ArrayList的add,remove方法。

// 这个是add里面的子方法
private void ensureExplicitCapacity(int minCapacity) {
      modCount++;

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

// 这个是remove(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;
    }
// 这是remove(Object o)
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
   }    

看完这几个方法,是不是都看到了modCount++这个操作?是的,添加和删除的时候都会对这个modCount加一。好了,我们在回头看看为啥test4()会抛出异常。
首先我们知道list里面add了三个元素所以modCount在添加完元素的时候是3,然后它开始遍历,当发现元素2的时候,去移除了元素2,此时modCount变为4.
我们在会到ArrayList的

@Override
   public void forEach(Consumer<? super E> action) {
       Objects.requireNonNull(action);
       final int expectedModCount = modCount;
       @SuppressWarnings("unchecked")
       final E[] elementData = (E[]) this.elementData;
       final int size = this.size;
       for (int i=0; modCount == expectedModCount && i < size; i++) {
           action.accept(elementData[i]);
       }
       if (modCount != expectedModCount) {
           throw new ConcurrentModificationException();
       }
   }

可以看到一开始把modCoun赋值给了expectedModCount,然后for循环里面和最后的if条件均对这个modCount有跑断,if里面如果发现modCount和expectedModCount不相等了,就抛出异常。当遍历到元素2,action.accept(elementData[i]);这一行执行完,再去遍历元素3的时候因为modCount == expectedModCount不相等了,所以循环推出,因此不会打印出找到元素3了,并且执行到if条件直接抛出异常。

5.迭代器

我们在来看看正确的使用方式,使用迭代器

public void test5() {
       Iterator<String> iterator = list.iterator();
       while (iterator.hasNext()) {
           String temp = iterator.next();
           if ("元素2".equals(temp)) {
               iterator.remove();
           }

           if ("元素3".equals(temp)) {
               System.out.println("找到元素3了");
           }
       }
   }
   // 这里打印出了 找到元素3了

上面的test3我们已经看到这个迭代器的代码了,那为啥它没问题呢?其实原理非常的沙雕,不信你看!

这是迭代器的remove方法

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

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
        // 看到没有直接把modeCount重新赋值给了expectedModCount 所以它们一直会相等啊
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

所以它是可以找到元素3的,因为直接忽略了remove对modCount的影响。

6.removeIf

jdk8还出了一种新写法,封装了在遍历元素时删除元素,removeIf(),如下:

 list.removeIf(item -> "元素2".equals(item)

点进去可以看到代码和上一个差不多只是封装了一层罢了。

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

总结:

1.我们探究了好几种遍历时删除元素的方式,也知道了fail-fast的基本概念。
2.学会了以后遍历的时候删除元素要使用迭代器哦,而且发现即使是单线程依然可以抛出ConcurrentModificationException
3.如果是多线程操作list的话建议使用CopyOnWriteArrayList,或者对迭代器加锁也行,看个人习惯吧~

相关文章
|
5月前
|
存储 Java
Java学习笔记 List集合的定义、集合的遍历、迭代器的使用
Java学习笔记 List集合的定义、集合的遍历、迭代器的使用
|
26天前
|
Java 机器人 程序员
从入门到精通:五种 List 遍历方法对比与实战指南
小米是一位热爱分享技术的程序员,本文详细介绍了 Java 中遍历 List 的五种方式:经典 for 循环、增强 for 循环、Iterator 和 ListIterator、Stream API 以及 forEach 方法。每种方式都有其适用场景和优缺点,例如 for 循环适合频繁访问索引,增强 for 循环和 forEach 方法代码简洁,Stream API 适合大数据量操作,ListIterator 支持双向遍历。文章通过生动的小故事和代码示例,帮助读者更好地理解和选择合适的遍历方式。
57 2
|
7月前
|
索引
List集合(方法简介,集合遍历)
List集合(方法简介,集合遍历)
|
3月前
|
Java 程序员 编译器
Java|如何正确地在遍历 List 时删除元素
从源码分析如何正确地在遍历 List 时删除元素。为什么有的写法会导致异常,而另一些不会。
68 3
|
7月前
|
Java 索引
JavaSE——集合框架一(3/7)-List系列集合:特点、方法、遍历方式、ArrayList集合的底层原理
JavaSE——集合框架一(3/7)-List系列集合:特点、方法、遍历方式、ArrayList集合的底层原理
49 2
|
8月前
List根据条件删除元素的几种方式
List根据条件删除元素的几种方式
129 0
|
8月前
|
索引 容器
06-python数据容器-list列表定义/list的10个常用操作/列表的遍历/使用列表取出偶数
06-python数据容器-list列表定义/list的10个常用操作/列表的遍历/使用列表取出偶数
|
7月前
|
安全 Java
java线程之List集合并发安全问题及解决方案
java线程之List集合并发安全问题及解决方案
1092 1
|
6月前
|
Java API Apache
怎么在在 Java 中对List进行分区
本文介绍了如何将列表拆分为给定大小的子列表。尽管标准Java集合API未直接支持此功能,但Guava和Apache Commons Collections提供了相关API。
|
6月前
|
运维 关系型数据库 Java
PolarDB产品使用问题之使用List或Range分区表时,Java代码是否需要进行改动
PolarDB产品使用合集涵盖了从创建与管理、数据管理、性能优化与诊断、安全与合规到生态与集成、运维与支持等全方位的功能和服务,旨在帮助企业轻松构建高可用、高性能且易于管理的数据库环境,满足不同业务场景的需求。用户可以通过阿里云控制台、API、SDK等方式便捷地使用这些功能,实现数据库的高效运维与持续优化。