移除List中的元素,你的姿式对了吗?

以前遇到对List进行遍历删除的时候,出现来一个 ConcurrentModificationException 异常,可能好多人都知道list遍历不能直接进行删除操做,可是你可能只是跟我同样知道结果,可是不知道为何不能删除,或者说这个报错是如何产生的,那么咱们今天就来研究一下。

1、异常代码

咱们先看下这段代码,你有没有写过相似的代码程序员

public static void main(String[] args) {

  List<Integer> list = new ArrayList<>();

  System.out.println("开始添加元素 size:" + list.size());

  for (int i = 0; i < 100; i++) {
    list.add(i + 1);
  }

  System.out.println("元素添加结束 size:" + list.size());

  Iterator<Integer> iterator = list.iterator();

  while (iterator.hasNext()) {
    Integer next = iterator.next();
    if (next % 5 == 0) {
      list.remove(next);
    }
  }
  System.out.println("执行结束 size:" + list.size());
}

「毫无疑问,执行这段代码以后,必然报错,咱们看下报错信息。」web

咱们能够经过错误信息能够看到,具体的错误是在checkForComodification 这个方法产生的。面试

2、ArrayList源码分析

首先咱们看下ArrayListiterator这个方法,经过源码能够发现,其实这个返回的是ArrayList内部类的一个实例对象。服务器

public Iterator<E> iterator() {
  return new Itr();
}

咱们看下Itr类的所有实现。微信

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

「参数说明:」app

cursor : 下一次访问的索引;编辑器

lastRet :上一次访问的索引;ide

expectedModCount :对ArrayList修改次数的指望值,初始值为modCount源码分析

modCount :它是AbstractList的一个成员变量,表示ArrayList的修改次数,经过addremove方法能够看出;测试

「几个经常使用方法:」

hasNext():

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

若是下一个访问元素的下标不等于size,那么就表示还有元素能够访问,若是下一个访问的元素下标等于size,那么表示后面已经没有可供访问的元素。由于最后一个元素的下标是size()-1,因此当访问下标等于size的时候一定没有元素可供访问。

next()

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

注意下,这里面有两个很是重要的地方,cursor初始值是0,获取到元素以后,cursor 加1,那么它就是下次索要访问的下标,最后一行,将i赋值给了lastRet这个其实就是上次访问的下标。

此时,cursor变为了1,lastRet变为了0。

最后咱们看下ArrayListremove()方法作了什么?

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
}

「重点:」

咱们先记住这里,modCount初始值是0,删除一个元素以后,modCount自增1,接下来就是删除元素,最后一行将引用置为null是为了方便垃圾回收器进行回收。

3、问题定位

到这里,其实一个完整的判断、获取、删除已经走完了,此时咱们回忆下各个变量的值:

cursor : 1(获取了一次元素,默认值0自增了1);

lastRet :0(上一个访问元素的下标值);

expectedModCount :0(初始默认值);

modCount :1(进行了一次remove操做,变成了1);

不知道你还记不记得,next()方法中有两次检查,若是已经忘记的话,建议你往上翻一翻,咱们来看下这个判断:

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

modCount不等于expectedModCount的时候抛出异常,那么如今咱们能够经过上面各变量的值发现,两个变量的值究竟是多少,而且知道它们是怎么演变过来的。那么如今咱们是否是清楚了ConcurrentModificationException异常产生的愿意呢!

「就是由于,list.remove()致使modCountexpectedModCount的值不一致从而引起的问题。」

4、解决问题

咱们如今知道引起这个问题,是由于两个变量的值不一致所致使的,那么有没有什么办法能够解决这个问题呢!答案确定是有的,经过源码能够发现,Iterator里面也提供了remove方法。

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,那么在调用next()进行检查判断的时候势必不会出现问题。

那么之后若是须要remove的话,千万不要使用list.remove()了,而是使用iterator.remove(),这样其实就不会出现异常了。

public static void main(String[] args) {

  List<Integer> list = new ArrayList<>();

  System.out.println("开始添加元素 size:" + list.size());

  for (int i = 0; i < 100; i++) {
    list.add(i + 1);
  }

  System.out.println("元素添加结束 size:" + list.size());

  Iterator<Integer> iterator = list.iterator();

  while (iterator.hasNext()) {
    Integer next = iterator.next();
    if (next % 5 == 0) {
      iterator.remove();
    }
  }
  System.out.println("执行结束 size:" + list.size());
}

「建议:」

另外告诉你们,咱们在进行测试的时候,若是找不到某个类的实现类,由于有时候一个类有超级多的实现类,可是你不知道它到底调用的是哪一个,那么你就经过debug的方式进行查找,是很便捷的方法。

5、总结

其实这个问题很常见,也是很简单,可是咱们作技术的就是把握细节,经过追溯它的具体实现,发现它的问题所在,这样你不只仅知道这样有问题,并且你还知道这个问题具体是如何产生的,那么从此不论对于你平时的工做仍是面试都是莫大的帮助。

本期分享就到这里,谢谢各位看到此处,

记得点个赞呦!

日拱一卒,功不唐捐

今日推荐

个人服务器接连被黑客攻击,我好难
致使MySQL索引失效的几种常见写法




本文分享自微信公众号 - 一个程序员的成长(xiaozaibuluo)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索