满屏飘红,操作ArrayList的Iterator方法时竟然给我报ConcurrentModificationException异常

988 阅读11分钟

前言

点赞在看,养成习惯。

点赞收藏,人生辉煌。

点击关注【微信搜索公众号:编程背锅侠】,防止迷路。

ArrayList系列文章

第一篇:ArrayList中的构造方法源码在面试中被问到了...抱歉没准备好!!!告辞
第二篇:面试官让我讲ArrayList中add、addAll方法的源码...我下次再来
第三篇:工作两年还没看过ArrayList中remove、removeAll、clear方法源码的都来报道吧
第四篇: 乱披风锤法锤炼ArrayList源码中的get、set、contains、isEmpty方法!!
第五篇:满屏飘红,操作ArrayList的Iterator方法时竟然给我报ConcurrentModificationException异常

删除方法表格

方法名 描述
public String toString() 把集合所有数据转换成字符串
public Iterator iterator() 普通迭代器
public int indexOf(Object o) 将集合清空
public int lastIndexOf(Object o) 删除与给定集合中相同的元素
default void remove() ArrayList内部的迭代器中的remove方法,删除集合中的元素
java.util.ConcurrentModificationException 迭代器中ConcurrentModificationException异常

public String toString()把集合所有数据转换成字符串

案例演示

@Test
public void test_toString(){
 ArrayList<String> list = new ArrayList<>();
 list.add("洛洛01");
 list.add("洛洛02");
  // 把集合所有数据转换成字符串
 String x = list.toString();
 System.out.println(x);
}

源码分析

// 把集合所有数据转换成字符串
public String toString() {
  // 注意:此时相当于用ArrayList对象在调用iterator()方法获取迭代器,那么这个时候需要先看看ArrayList中的iterator()方法,ArrayList自己实现了List中的Iterator方法
 Iterator<E> it = iterator();
  // 调用ArrayList中hasNext方法判断是否有元素,如果hasNext()方法返回false 
  // 那么就toString方法就返回一个 "[]"
 if (! it.hasNext())
  return "[]";
  // 创建StringBuilder,对集合的内容进行拼接,避免字符串频繁拼接产生很多无效对象
 StringBuilder sb = new StringBuilder();
  // 拼接字符串的左侧中括号
 sb.append('[');
  // 无限循环遍历
 for (;;) {
    // 调用ArrayList中next方法取出元素
  E e = it.next();
  sb.append(e == this ? "(this Collection)" : e);
    // 调用ArrayList中hasNext方法判断是否有元素,如果hasNext()方法返回false 
    // 那么就拼接一个 "]"并转换为toString返回
  if (! it.hasNext())
   return sb.append(']').toString();
    // 拼接元素分隔符【, 】逗号、空格
  sb.append(',').append(' ');
 }
}

// ArrayList中的Iterator方法,ArrayList集合内部类
public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
 
  // 返回ArrayList集合内部Iterator类的对象
  public Iterator<E> iterator() {
  return new Itr();
 }

  // 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
  // 将实际修改集合次数赋值给预期修改次数 ,注意只会赋值一次 
    // 以后在迭代器获取元素的时候,每次都会判断集合实际修改次数是否和预期修改次数一致 
    // 如果不一致就会产生并发修改异常
    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 = this.elementData;
   if (i >= elementData.length)
    throw new ConcurrentModificationException();
   cursor = i + 1;
   return (E) elementData[lastRet = i];
  }

    // 并发修改异常
  final void checkForComodification() {
      // 如果预期修改次数 和 实际修改次数不相等 就产生并发修改异常
   if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
  }
 }
}

总结

返回此集合的字符串表示形式。如果集合为空,则返回"[]"。如果集合中有元素,则返回"[元素, 元素]"

','和空格分隔的字符串列表。

public Iterator iterator()普通迭代器

案例演示

@Test
public void test_toString_ie(){
 ArrayList<String> list = new ArrayList<>();
 list.add("洛洛01");
 list.add("洛洛02");
 list.add("洛洛03");
  // 获取迭代器
 Iterator<String> iterator = list.iterator();
  // 遍历集合
 while (iterator.hasNext()){
  String next = iterator.next();
  System.out.println(next);
 }
}

源码分析

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
 
  // ArrayList内部类
  // 一定要注意观察 Itr 类中的几个成员变量
  public Iterator<E> iterator() {
  return new 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
    //将实际修改集合次数 赋值 给预期修改次数 
    //在迭代的过程中,只要实际修改次数和预期修改次数不一致就会产生并发修改异常 
    //由于expectedModCount是Itr的成员变量,那么只会被赋值一次!!! 
    //同时由于集合调用了三次add方法,那么实际修改集合次数就是 3,因此expectedModCount的值也是 3
  int expectedModCount = modCount;

  Itr() {}

    // 判断是否有下一个元素
  public boolean hasNext() {
   return cursor != size;
  }
    
    // 获取下一个元素
  @SuppressWarnings("unchecked")
  public E next() {
      // 校验修改的次数和预期的次数是否相等,不相等抛出ConcurrentModificationException异常
   checkForComodification();
      // 把下一个元素的索引赋值给i
   int i = cursor;
      // 如果这个索引大于集合的长度,抛出NoSuchElementException异常
   if (i >= size)
    throw new NoSuchElementException();
      // 将集合底层存储数据的数组赋值给迭代器的局部变量 elementData
   Object[] elementData = ArrayList.this.elementData;
      // 再次判断,如果下一个元素的索引大于集合底层存储元素的长度 并发修改异常 
      // 注意,尽管会产生并发修改异常,但是这里显示不是我们要的结果
   if (i >= elementData.length)
    throw new ConcurrentModificationException();
      // 每次成功获取到元素,下一个元素的索引都是当前索引+1
   cursor = i + 1;
      // 返回元素
   return (E) elementData[lastRet = i];
  }

  final void checkForComodification() {
      // 如果预期修改次数 和 实际修改次数不相等 就产生并发修改异常
   if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
  }
 }
}

总结

使用迭代器循环遍历这个集合,这个迭代器是ArrayList源码里面的迭代器。hasNext方法是用来判断是否有下一个元素存在,next方法是获取元素。

modCount修改的次数和expectedModCount预期的次数需要关注一下。

public int indexOf(Object o)查询给定元素在集合中首次出现的索引

案例演示

@Test
public void test_indexOf(){
 ArrayList<String> list = new ArrayList<>();
 list.add("洛洛01");
 list.add("洛洛02");
 list.add("洛洛02");
 list.add("洛洛03");

 int i = list.indexOf("洛洛02");
 System.out.println(i);// 1
}

源码分析

// 根据指定的值o,获取这个值在集合中的索引
public int indexOf(Object o) {
  // 判断这值是否为null
 if (o == null) {
    // 循环遍历这个集合
  for (int i = 0; i < size; i++)
      // 找到集合中第一个值为null,并返回这个值的索引
   if (elementData[i]==null)
    return i;
 } else {
    // 给定的值o不为null,循环遍历这个集合
  for (int i = 0; i < size; i++)
      // 判断集合中是否有给定的相等元素,如果有就返回这个值的索引
   if (o.equals(elementData[i]))
    return i;
 }
  // 集合中不存在这个给定的值o,返回-1
 return -1;
}

总结

根据指定的值o,获取这个值在集合中的索引。判断这个集合中是否存在给定的元素,如果没有返回-1;如果有就返回第一次查找到这个元素的索引。

public int lastIndexOf(Object o)查询给定好元素在集合中最后一次出现的索引

案例演示

@Test
public void test_lastIndexOf(){
 ArrayList<String> list = new ArrayList<>();
 list.add("洛洛01");
 list.add("洛洛03");
 list.add("洛洛02");
 list.add("洛洛03");
 list.add("洛洛03");
  // 获取集合中与"洛洛03"相等的元素,倒序遍历
 int i = list.lastIndexOf("洛洛03");
 System.out.println(i);// 4
}

源码分析

// // 根据指定的值o,获取这个值在集合中的索引
public int lastIndexOf(Object o) {
  // 判断这值是否为null
 if (o == null) {
    // 给定的值为null,循环遍历这个集合,倒序遍历的
  for (int i = size-1; i >= 0; i--)
      // 判断是否有null,有了返回这个索引i
   if (elementData[i]==null)
    return i;
 } else {
    // 给定的值不为null,倒序遍历集合
  for (int i = size-1; i >= 0; i--)
      // // 判断集合中是否有给定的相等元素,如果有就返回这个值的索引,从后往前找这个相等值
   if (o.equals(elementData[i]))
    return i;
 }
  // 集合中没有给定的值,返回-1
 return -1;
}

总结

根据指定的值o,获取这个值在集合中的索引。判断这个集合中是否存在给定的元素,如果没有返回-1;如果有就返回最后一次查找到这个元素的索引。源码中的循环是倒序遍历。

default void remove()迭代器中的remove方法,删除集合中的元素

代码演示

@Test
public void test_iterator_remove(){
 ArrayList<String> list = new ArrayList<>();
 list.add("洛洛01");
 list.add("洛洛02");
 list.add("洛洛02");
 list.add("洛洛03");
  // 这一坨代码将在下面使用removeIf方法代替,jdk的lambda表达式代替
 Iterator<String> iterator = list.iterator();
 while (iterator.hasNext()){
  String next = iterator.next();
  if (next.equals("洛洛02")){
   iterator.remove();
  }
 }
 list.forEach(System.out::println);
}

// 【removeIf】方法的使用是不是很爽可以简化很多代码
@Test
public void test_iterator_remove(){
 ArrayList<String> list = new ArrayList<>();
 list.add("洛洛01");
 list.add("洛洛02");
 list.add("洛洛02");
 list.add("洛洛03");
 list.removeIf(next -> next.equals("洛洛02"));
 list.forEach(System.out::println);
}

源码分析

public class ArrayList<E> extends AbstractList<E>
  implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

 // ArrayList内部类
 // 一定要注意观察 Itr 类中的几个成员变量
 public Iterator<E> iterator() {
  return new 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
  // 将实际修改集合次数赋值给预期修改次数 
  // 在迭代的过程中,只要实际修改次数和预期修改次数不一致就会产生并发修改异常 
  // 由于expectedModCount是Itr的成员变量,那么只会被赋值一次!!! 
  // 同时由于集合调用了三次add方法,那么实际修改集合次数就是 3,因此expectedModCount的值也是 4
  int expectedModCount = modCount;
    // 没有参数的构造方法
  Itr() {}
    
    // 【重点这个方法】迭代器删除元素方法
  public void remove() {
      // 判断最后返回元素的索引是否小于0,满足条件就产生【非法状态异常】
   if (lastRet < 0)
    throw new IllegalStateException();
      // 校验是否会产生并发修改异常,第一次调用不会,因为与其修改次数和实际修改次数一致
   checkForComodification();

   try {
        // 真正删除集合元素的方法,【调用方法为ArrayList的方法remove】,且将0作为参数进行传递
        // 这个方法的源码分析在ArrayList系列文章的另一篇里面可以去看一下
    ArrayList.this.remove(lastRet);
        // 将lastRet赋值给cursor
    cursor = lastRet;
        // 再次等于-1
    lastRet = -1;
        // 再次将集合实际修改次数赋值给预期修改次数,那么这个时候不管集合自身是否删除成功 
        // 那么实际修改次数和预期修改次数又一致了,所以并不会产生并发修改异常
    expectedModCount = modCount;
   } catch (IndexOutOfBoundsException ex) {
    throw new ConcurrentModificationException();
   }
  }

 }
}

迭代器中的java.util.ConcurrentModificationException异常的产生

案例演示

@Test
public void test_toString_CME_01(){
 List<String> list = new ArrayList<>();
 list.add("洛洛01");
 list.add("洛洛02");
 list.add("洛洛03");
 Iterator<String> iterator = list.iterator();
 while (iterator.hasNext()){
  String next = iterator.next();
  if (next.equals("洛洛03")){
      // 这一行代码会引起ConcurrentModificationException异常
      // 为什么这一行代码会引起ConcurrentModificationException异常呢?
      // 这其中发生了什么?
   list.remove("洛洛03");
  }
 }
 System.out.println(list.toString());
}

// 针对以上会出现ConcurrentModificationException异常,可以使用iterator自己的remove方法避免这种异常
@Test
public void test_toString_CME_01(){
 List<String> list = new ArrayList<>();
 list.add("洛洛01");
 list.add("洛洛02");
 list.add("洛洛03");
 Iterator<String> iterator = list.iterator();
 while (iterator.hasNext()){
  String next = iterator.next();
  if (next.equals("洛洛03")){
      // 这个方法可以解决上面的异常
   iterator.remove();
  }
 }
 System.out.println(list.toString());
}

产生异常的源码

final void checkForComodification() {
  // 集合修改的次数,不等于期望修改的次数expectedModCount抛出ConcurrentModificationException异常
 if (modCount != expectedModCount)
    // 异常就是从这个地方来的
  throw new ConcurrentModificationException();
}

异常提示

java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
 at java.util.ArrayList$Itr.next(ArrayList.java:859)

异常分析

查看修改的次数modCount的值和期望修改的值expectedModCount,这两个值的变化

![image-20200706184112106](/Users/fenke/Library/Application Support/typora-user-images/image-20200706184112106.png)

总结

  • 针对上面的案例以及两张图,debug测试会发现iterator.next()方法报错了,其实是checkForComodification方法报的错。

  • 针对以上的案例演示,报异常的原因expectedModCount为3。这个值其实来源于ArrayList源码中的内部类Itr,他的值在调用list.iterator()方法的时候已经给定。

  • 但是modCount我们一共修改了4次,其中3次add和1次remove操作。

  • 就是最后一次的这个list.remove操作导致的这个异常。这个方法是ArrayList的方法。操作这个的modCount方法不会同步给Itr中的expectedModCount。所以这两个值不相等,就报了这个异常。可以使用Itr中自己的remove方法。

  • 异常出现的位置是String next = iterator.next();这一行代码。其实还有一个点就是为什么iterator.hasNext()执行成功了,这个留给各位看官自己去想,哈哈哈😂。

创作不易, 非常欢迎大家的点赞、评论和关注(^_−)☆
你的点赞、评论以及关注是对我最大的支持和鼓励,而你的支持和鼓励
我继续创作高质量博客的动力 !!!

掘金征文 | 2020 与我的年中总结 征文活动正在进行中......