资讯专栏INFORMATION COLUMN

Java 迭代器引发 ConcurrentModificationException

kumfo / 3483人阅读

摘要:迭代器智能吗第一步,将列表中的根节点找出来。源码翻开中迭代器的源码。在迭代器对象执行操作之前,都会执行方法,以判断当前操作下是否安全。

引言

ConcurrentModificationException这个异常大家都很熟悉,当在forEach进行删除时都会出现该异常。

如果你还不了解,请参考澍澍的博客:关于在list循环的过程中进行删除的处理 - 晨澍的博客

ConcurrentModificationException的解决方案之一是使用迭代器,但是不代表迭代器就一劳永逸了。

使用的时候还需斟酌数组的索引。

描述 问题场景

如下图所示:

原来的同步方法获取的节点是节点的父节点,根据父节点进行对应。

然而在同步更新文件的时候,发现这样并不好处理,在不改动原代码的情况下,设计了将列表转为树型结构的方法,这样可以从根节点向下开始遍历,便于操作。

也是在牛客网身经百战,实现这个难度不大。但在编写相关实现的时候,遇到了一个小问题。

迭代器智能吗?

第一步,将列表中的根节点找出来。

@Override
public ClusterNode listToTree(List clusterNodes) {
    logger.debug("声明根节点名称");
    final String ROOT_NAME = "ROOT";

    logger.debug("声明根节点");
    ClusterNode rootNode = null;

    logger.debug("获取迭代器,遍历节点列表");
    Iterator iterator = clusterNodes.iterator();
    while (iterator.hasNext()) {

        logger.debug("向后遍历");
        ClusterNode clusterNode = iterator.next();

        if (ROOT_NAME.equals(clusterNode.getName())) {
            logger.debug("获取到根节点,赋值,并从原列表中移除");
            rootNode = clusterNode;
            iterator.remove();
            break;
        }
    }

    logger.debug("设置子节点");
    assert rootNode != null;
    setChildrenNode(rootNode, clusterNodes);

    return rootNode;
}

第二步,再从根节点开始,递归设置子节点。

/**
 * 为节点设置符合条件的子节点,同时递归,设置子节点的子节点
 * @param parentNode   父节点
 * @param clusterNodes 子节点列表
 */
private void setChildrenNode(ClusterNode parentNode, List clusterNodes) {
    logger.debug("清空原集合");
    parentNode.getClusterNodes().clear();

    logger.debug("遍历列表");
    Iterator iterator = clusterNodes.iterator();
    while (iterator.hasNext()) {
        ClusterNode clusterNode = iterator.next();

        logger.debug("如果父节点匹配");
        if (clusterNode.getParentClusterNode().getName().equals(parentNode.getName())) {
            logger.debug("将当前节点添加到父节点的子列表中");
            parentNode.getClusterNodes().add(clusterNode);

            logger.debug("移除该节点");
            iterator.remove();

            logger.debug("递归设置子节点");
            setChildrenNode(clusterNode, clusterNodes);
        }
    }
}

思想肯定是没问题的。

调用了一行递归,当我在编写这行代码的时候就已经察觉到可能不对了,因为本次迭代器还没有迭代完,条件符合时就去递归,然后递归又去新建迭代器,递归中又可能删除新元素,再递归回来,继续迭代的结果还正确吗?

logger.debug("递归设置子节点");
setChildrenNode(clusterNode, clusterNodes);

本着完全相信JDK的思想,兴许我忧虑的事情,其实大牛们早就帮我解决了呢?虽然感觉可能不对,但还是这样写了。想着把测试用例写得完善一些,错了再改呗!

测试

一测试,果然出错了!在调用next方法时抛出了ConcurrentModificationException异常,看来,迭代器还没有智能到如此地步。

源码

翻开ArrayList中迭代器的源码。(自从上次在慕课网的驱动下,强制自己阅读了HashMap的源码后,发现自己对读源码没那么抗拒了。)

在刷过编程题后,终于明白为什么这些家公司都问HashMap源码,HashMap真的是太重要了,可以在实际业务中大大降低算法的时间复杂度!

/**
 * Returns an iterator over the elements in this list in proper sequence.
 *
 * 

The returned iterator is fail-fast. * * @return an iterator over the elements in this list in proper sequence */ public Iterator iterator() { return new Itr(); }

迭代器方法内部就返回了一个Itr的对象,这是ArrayList中的私有内部类,为什么要用内部类呢?一大好处就是内部类可以直接访问外部类的私有成员,具体进行集合操作的时候非常方便。

ConcurrentModificationException,因为十分常见,所以我们常常只关注了这个异常怎么出现的,往往忽略了异常本身。

并发修改异常,最简单的场景,就是线程不安全的多线程并发场景。

在迭代器对象执行操作之前,都会执行checkForComodification方法,以判断当前操作下是否安全。就像下面的代码实现一样,判断当前的数量是否是我预期的数量。

如果不是,表示有人动过我的列表,抛异常,不干了。

如果是,表示没有人动过我的列表,才继续执行。

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

既然出现了问题,怎么解决呢?

那就不要在遍历的过程中再去递归修改了,等他遍历完再说。添加一个待处理的列表,遍历之后再递归,保证每次迭代都不冲突。

logger.debug("下一次需要处理的父节点");
List nextParentNodes = new ArrayList<>();

logger.debug("遍历列表");
Iterator iterator = clusterNodes.iterator();
while (iterator.hasNext()) {
    ClusterNode clusterNode = iterator.next();

    logger.debug("如果父节点匹配");
    if (clusterNode.getParentClusterNode().getName().equals(parentNode.getName())) {
        logger.debug("将当前节点添加到父节点的子列表中");
        parentNode.getClusterNodes().add(clusterNode);

        logger.debug("移除该节点");
        iterator.remove();

        logger.debug("添加到下一次父节点中");
        nextParentNodes.add(clusterNode);
    }
}

logger.debug("递归处理所有待处理的父节点");
for (ClusterNode clusterNode : nextParentNodes) {
    setChildrenNode(clusterNode, clusterNodes);
}
总结

迭代器并非一劳永逸,保证多次修改的独立才是最佳实践。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/75264.html

相关文章

  • Java 迭代引发 ConcurrentModificationException

    摘要:迭代器智能吗第一步,将列表中的根节点找出来。源码翻开中迭代器的源码。在迭代器对象执行操作之前,都会执行方法,以判断当前操作下是否安全。 引言 ConcurrentModificationException这个异常大家都很熟悉,当在forEach进行删除时都会出现该异常。 如果你还不了解,请参考澍澍的博客:关于在list循环的过程中进行删除的处理 - 晨澍的博客 showImg(http...

    用户83 评论0 收藏0
  • java集合

    摘要:主要用于遍历集合中的元素,对象也被称为迭代器。使用迭代过程中,不可修改集合元素迭代器采用快速失败机制。一旦迭代过程中检测到该集合已经被修改,程序立即出发异常,而不是显示修改后的结果,避免了共享资源而引发的潜在问题。 集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量);而集合里只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集...

    JinB 评论0 收藏0
  • Java 集合 Collection、Iterator

    摘要:如果需要创建对象,则必须与一个被迭代的集合。这是一个有状态的方法该方法用于保证对该流的后续访问中最大允许访问的元素个数。可以对集合元素进行整体的聚集操作。 Java集合分为Set(无序、不可重复)、List(有序、重复)、Queue(队列)和Map(映射关系) Java集合概述 数组元素既可以是基本类型的值,也可以是对象(实际保存对象的引用变量)集合只能保存对象(实际保存对象的引用变量...

    harryhappy 评论0 收藏0
  • fail-fast与fail-safe在Java集合中的应用

    摘要:与在迭代器中的设计在中,最典型的与就是关于迭代器的设计。缺点是,迭代器不能正确及时的反应集合中的内容,而且一定程度上也增加了内存的消耗。 fail-fast与fail-safe简介 如果一个系统,当有异常或者错误发生时就立即中断执行,这种设计称之为fail-fast。相反如果我们的系统可以在某种异常或者错误发生时继续执行,不会被中断,这种设计称之为fail-safe。 fail-fas...

    Drummor 评论0 收藏0
  • java集合--Collection接口

    摘要:集合的元素个数为输出集合的元素个数为在本代码中,新建一个局部变量保存的成员方法返回的值,输出得到因为只有一个元素。注若遍历集合的同时改变集合,将引发异常。     在概述里面也说过:Collection是java集合两大接口之一,旗下有三大子接口:Set(元素不能重复,且无序)、Queue、List(元素可重复,且有序)。    Collection来源于java.util包,主要方法...

    k00baa 评论0 收藏0

发表评论

0条评论

kumfo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<