通过这个小例子了解几个知识:
(1)ArrayList的内部Iterator理解
(2)modCount的用途
(3)为什么会ConcurrentModificationException以及原理
(4)有什么办法实现遍历中remove
代码例子:
运行结果:
查看生成的class文件:
(1)ArrayList的内部Iterator理解:
其实foreach语法糖在实际执行的时候是通过迭代器(Iterator)实现的,
主要用了上图的hasNext()和next()
看一下ArrayList的Iterator。
返回了内部类Itr
下面是itr 源码:
/**
* An optimized version of AbstractList.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;
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()和hasNext方法的具体实现都可以看出。
lastRet:表示上一个访问的元素的索引
expectedModCount:表示对ArrayList修改次数的期望值,它的初始值为modCount。
modCount是AbstractList类中的一个成员变量:他代表着list被修改的次数初始值为0
protected transient int modCount = 0;
该值表示对List的修改次数,add()、remove()、addAll()、removeRange()
及clear()方法。这些方法每调用一次,modCount的值就加1。注:add()及
addAll()方法的modCount的值是在其中调用的ensureCapacity()方法中增
加的。
(2)modCount的用途(为什么需要它):
在java的集合类中常见的变量modCount,用于记录对象的修改次数,
比如增、删、改,也基本存在于非线程安全的集合类中
有一点版本控制的意思,可以理解成version,在特定的操作下需要
对version进行检查,适用于Fail-Fast机制。
在java的集合类中存在一种Fail-Fast的错误检测机制,当多个线程对
同一集合的内容进行操作时,可能就会产生此类异常。
比如当A通过iterator去遍历某集合的过程中,其他线程修改了此集合
,此时会抛出ConcurrentModificationException异常。
此类机制就是通过modCount实现的,在迭代器初始化时,会赋值
expectedModCount,在迭代过程中判断modCount和expectedModCount、
是否一致;
(3)为什么会ConcurrentModificationException以及原理:
通过对上面基础信息的理解,下面来一点一点走,寻找问题
上面程序:
第一步(图中圈1)
当调用platformList.iterator()返回一个Iterator
第二步(图中圈2)
通过Iterator的hashNext()方法判断是否还有元素未被访问
关于hasNext()方法的实现:
第三步(图中圈3)
调用next()方法获取,看一下next的实现
进来首先调用的这个方法
核心逻辑就是比较modCount和expectedModCount这两个变量的值
Debug看一下全流程:
通过三次add
由于调用了三次add所以list的Size是3,同时modCount也是3
所以此时在执行圈1的时候,由下图可知expectedModCount也是等于3:
由于第一次进入循环此时expectedModCount==modCount
所以
这个校验是没有问题的——通过下图圈1校验;
然后next()这个方法中还把cursor(当前元素索引)赋值给lastRet(前一个元素索引):圈3,然后增加当前元素索引:圈2,并且返回当前的元素:圈4。
经过上面一顿操作此时
第四步(图中圈4)执行remove
从.Class文件点进去。发现调用的remove是ArrayList下面的remove,并不是迭代器的remove或者说并不是内部类Itr中的remove(Itr实现Iterator)
调用的是这个:而这个调用了fastRemove
fastRemove中对modCount进行了加一操作而此时注意expectedModCount是没有变的,此时两个值:
modCount:4
expectedModCount:3
此时执行完第一次循环:
当进入第二次循环的时候,再次调用next方法的时候,还是会校验这两个值:
但是此时两个值已经不相等:所以抛出异常:ConcurrentModificationException
(4)有什么办法实现遍历中remove(极不推荐,很危险,多机环境或者高并发场景都可能出问题,并且阿里开发规范明确要求禁止在遍历时删除元素,正确的做法是遍历的时候标记出来,后续代码再用也方便)
1、使用Iterator的remove()方法
现在想一想 为啥迭代器的remove就没事呢:看下源码
圈1的地方调用了ArrayList里面的remove,想一下上面的操作。ArrayList的remove仅仅是增加了modCount,没有增加expectedModCount,然后导致异常,因此在这里调用完这个ArrayList的remove,在主动同步一下expectedModCount:上图圈2,这样使得modCount==expectedModCount再次进入循环的时候调用检查方法就不会异常了:
2、使用for循环正序遍历
public static void main2(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客园");
platformList.add("优快云");
platformList.add("掘金");
for (int i = 0; i < platformList.size(); i++) {
String item = platformList.get(i);
if (item.equals("博客园")) {
platformList.remove(i);
i = i - 1;
}
}
System.out.println(platformList);
}
这种实现方式比较好理解,就是通过数组的下标来删除,不过有个注意事项就是删除元素后,要修正下下标的值:
为什么要修正下标的值呢?
因为刚开始元素的下标是这样的:
第1次循环将元素"博客园"删除后,元素的下标变成了下面这样:
这样导致所有的索引都少了1,所以需要进行修正下标值
3、使用for循环倒序遍历
public static void main3(String[] args) {
List<String> platformList = new ArrayList<>();
platformList.add("博客园");
platformList.add("优快云");
platformList.add("掘金");
for (int i = platformList.size() - 1; i >= 0; i--) {
String item = platformList.get(i);
if (item.equals("掘金")) {
platformList.remove(i);
}
}
System.out.println(platformList);
}
这种实现方式和使用for循环正序遍历类似,不过不用再修正下标,因为刚开始元素的下标是这样的:
第1次循环将元素"掘金"删除后,元素的下标变成了下面这样:
不影响原来数据的索引值,所以不需要修正。
回头注意一下,后两种使用for循环,并通过索引操作,并没有被转译成迭代器遍历,来看看class文件:
正序移除:
倒序移除: