foreach去除重复元素java_Java foreach 中List移除元素抛出ConcurrentModificationException原因全解析...

本文深入探讨了在Java中使用foreach循环删除List元素时引发的`ConcurrentModificationException`异常,解释了错误的根本原因,并提供了解决方案。分析了迭代器的工作原理,展示了反例代码及其不同运行结果,强调了正确使用`iterator.remove()`或`removeIf()`方法的重要性。
摘要由CSDN通过智能技术生成

本文重点探讨 foreach 循环中List 移除元素造成 java.util.ConcurrentModificationException 异常的原因。

先看《阿里巴巴 Java开发手册》中的相关规定:

AAffA0nNPuCLAAAAAElFTkSuQmCC

那么思考几个问题:反例的运行结果怎样?

造成这种现象的根本原因是什么?

有没有更优雅地的移除元素姿势?

本文将为你深度解读该问题。

2.0 反例源代码

public class ListExceptionDemo {

public static void main(String[] args) {

List list = new ArrayList<>();

list.add("1");

list.add("2");

for (String item : list) {

if ("1".equals(item)) {

list.remove(item);

}

}

}

}

2.1 反例的运行结果

当 if 的判断条件是 “1”.equals(item) 时,程序没有抛出任何异常。if ("1".equals(item)) {

list.remove(item);

}

而当判断条件是 :"2".equals(item)时,运行会报 java.util.ConcurrentModificationException。

2.2 原因分析

2.2.1 错误提示

既然报错,那么好办,直接看错误提示呗。Exception in thread "main" java.util.ConcurrentModificationException

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

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

at com.chujianyun.common.collection.list.ListExceptionDemo.main(ListExceptionDemo.java:13)

啥 ConcurrentModificationException? 并发修改异常? 一个线程哪来的并发呢?

对应的时序图

AAffA0nNPuCLAAAAAElFTkSuQmCC

然后我们通过错误提示看源码:我们看到错误的原因是执行 ArrayList的 Itr.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];

}

modCount 和 expectedModCount不一致导致的:final void checkForComodification()

{

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

}

因此可以推测出发生异常的根本原因在于:取下一个元素时,检查 modCount,发现不一致。

2.2.2 代码调试法

为了验证上面的推测,大家可以在上述两个关键函数上打断点,通过单步了解程序的运行步骤。

我们通过调试可以“观察到”,ArrayList中的 foreach 循环的语法糖最终迭代器Array$Itr 实现的。

通过断点我们发现,ArrayList 构造内部类 Itr 对象时 expectedModCount 的值为 ArrayList的 modCount。

运行 next 函数时会检查List 中的 modCount 的值 和 构造迭代器时“备份的” expectedModCount 是否相等。

AAffA0nNPuCLAAAAAElFTkSuQmCC

通过调试我们还发现:虽然原始 list 至于两个元素,for each 循环执行两次后,满足if 条件移除 值为“2”的元素之后, foreach 循环依然可以进入,此时会再次通过 next 取出 list中的元素,又会执行  checkForComodification函数检查上述两个值是否相等,此时不等,抛出异常。

AAffA0nNPuCLAAAAAElFTkSuQmCC

那么这里有存在两个问题:为什么 List 为 2  , next 却执行了 3 次呢?

如果不通过调试我们怎么知道 foreach 语法糖的底层如何实现的呢?

带着这两个问题,我们继续深入研究下去。

2.2.3  源码解析

我们查看  ArrayList$Itr 的 hasNext 函数:private class Itr implements Iterator {

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;

}

// 其他省略

}

发现ArrayList的迭代器判断是否有下一个元素的标准是将下一个待返回的元素的索引和 size 比,不等表示还有下一个元素。

我们重新看源码:public static void main(String[] args) {

List list = new ArrayList<>();

list.add("1");

list.add("2");

for (String item : list) {

if ("2".equals(item)) {

list.remove(item);

}

}

}

最初 List 中有两个元素,expectedModCount  值为2。

遍历第一个时没有走到if, 遍历第二个元素时走到if ,通过 List.remove 函数移除了元素。public boolean remove(Object o) {

if (o == null) {

for (int index = 0; index 

if (elementData[index] == null) {

fastRemove(index);

return true;

}

} else {

for (int index = 0; index 

if (o.equals(elementData[index])) {

fastRemove(index);

return true;

}

}

return false;

}

而remove会调用 fastRemove 函数实际移除掉元素,在此函数中会将 modCount+1,即 modCount的值为3。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

}

因此在次进入foreach 时,expectedModCount  值 和 modCount的值 不相等,因此认为还有下一个元素。

但是调用迭代器的 next 函数时需检查两者是相等,发现不等,抛出ConcurrentModificationException异常。

当 if条件是  “1”.equals(item)时public static void main(String[] args) {

List list = new ArrayList<>();

list.add("1");

list.add("2");

for (String item : list) {

if ("1".equals(item)) {

list.remove(item);

}

}

}

循环取出第一个元素后直接通过list给移除掉了,再次进入 foreach循环时,通过 hashNext 判断是否有下一个元素时,由于 游标==1(此时list的 size),因此判断没下一个元素。

也就是说此时循环只执行了一次就结束了,没有走到可以抛出ConcurrentModificationException异常的任何函数中,从而没有任何错误。

读到这里对迭代器的理解是不是又深了一层呢?

看到这里可能还有些同学对 foreach 究竟底层怎么实现的仍然一知半解,那么请看下一部分。

2.2.4 反汇编

话不多说,直接反汇编:public class com.chujianyun.common.collection.list.ListExceptionDemo {

public com.chujianyun.common.collection.list.ListExceptionDemo();

Code:

0: aload_0

1: invokespecial #1                  // Method java/lang/Object."":()V

4: return

public static void main(java.lang.String[]);

Code:

0: new           #2                  // class java/util/ArrayList

3: dup

4: invokespecial #3                  // Method java/util/ArrayList."":()V

7: astore_1

8: aload_1

9: ldc           #4                  // String 1

11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

16: pop

17: aload_1

18: ldc           #6                  // String 2

20: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z

25: pop

26: aload_1

27: invokeinterface #7,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;

32: astore_2

33: aload_2

34: invokeinterface #8,  1            // InterfaceMethod java/util/Iterator.hasNext:()Z

39: ifeq          72

42: aload_2

43: invokeinterface #9,  1            // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;

48: checkcast     #10                 // class java/lang/String

51: astore_3

52: ldc           #6                  // String 2

54: aload_3

55: invokevirtual #11                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z

58: ifeq          69

61: aload_1

62: aload_3

63: invokeinterface #12,  2           // InterfaceMethod java/util/List.remove:(Ljava/lang/Object;)Z

68: pop

69: goto          33

72: return

}

代码偏移从 0 到 25 行实现下面这部分功能:List list = new ArrayList<>();

list.add("1");

list.add("2");

从 26行开始我们发现底层使用迭代器实现,我们脑补后翻译回 Java代码大致如下:public static void main(String[] args) {

List list = new ArrayList<>();

list.add("1");

list.add("2");

Iterator iterator = list.iterator();

while (iterator.hasNext()) {

String item = iterator.next();

if ("2".equals(item)) {

//iterator.remove();

list.remove(item);

}

}

}

大家运行“翻译”后的代码发信啊和原始代码的报错内容完全一致:Exception in thread "main" java.util.ConcurrentModificationException

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

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

at com.chujianyun.common.collection.list.ListException.main(ListException.java:16)

2.2.5 继续深挖

1、为啥通过 iterator.remove() 移除元素就没事呢?

我们看 java.util.ArrayList.Itr#remove 的源码:public void remove() {

if (lastRet 

throw new IllegalStateException();

checkForComodification();

try {

ArrayList.this.remove(lastRet);

cursor = lastRet;

lastRet = -1;

expectedModCount = modCount;

} catch (IndexOutOfBoundsException ex) {

throw new ConcurrentModificationException();

}

}

从这里我们看到,通过迭代器移除元素后, expectedModCount 会重新赋值为 modCount。

因此使用iterator.remove() 移除元素不报错的原因就找到了。

2、有没有比手册给出的代码更优雅的写法?

我们打开其函数列表,观察List 和其父类有没有便捷地移除元素方式:

“惊奇”地发现,Collection 接口提供了 removeIf 函数可以满足此需求。

还等啥呢,替换下,发现代码如此简洁:public static void main(String[] args) {

List list = new ArrayList<>();

list.add("1");

list.add("2");        // 一行代码实现

list.removeIf("2"::equals);

}

自此是不是文章就该结束了呢?

NO..

removeIf 为啥能够实现移除元素的功能呢?

我们猜测,底层应该是遍历然后对比元素然后移除,可能也是迭代器方式,我们看源码:

java.util.Collection#removeIfdefault boolean removeIf(Predicate super E> filter) {

Objects.requireNonNull(filter);

boolean removed = false;

final Iterator each = iterator();

while (each.hasNext()) {

if (filter.test(each.next())) {

each.remove();

removed = true;

}

}

return removed;

}

我们发现和我们想的比较一致。

本小节对《阿里巴巴 Java开发手册》中 foreach 循环 List 移除元素导致并发修改异常的问题,进行了全面深入地剖析。

希望可以帮助大家,彻底搞懂这个问题。

另外也提供了研究类似问题的一般思路,即代码调试、读源码、反汇编等。

通过这个问题,希望大家遇到问题时,能够养成深挖的精神,通过问题带动知识的理解,知其所以然。

最后提醒大家,不要看书记结论,容易忘,记住不会用,要多思考原因,才能理解更深刻。

“尽信书不如无书”,不要认为作者写的都是对的,都是最好的,要有自己的思考。

想了解更多《手册》详解的更多内容,想学习更多开发和避坑技巧等,请关注《阿里巴巴Java 开发手册》详解专栏。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值