转载

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

导语:

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

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

首先提出一个概念: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我们已经看到这个迭代器的代码了,那为啥它没问题呢?其实原理非常的沙雕,不信你看!

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

这是迭代器的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,或者对迭代器加锁也行,看个人习惯吧~

原文  https://segmentfault.com/a/1190000022373495
正文到此结束
Loading...