ConcurrentModificationException
这个异常大家都很熟悉,当在 forEach
进行删除时都会出现该异常。
如果你还不了解,请参考澍澍的博客: 关于在list循环的过程中进行删除的处理 - 晨澍的博客
ConcurrentModificationException
的解决方案之一是使用迭代器,但是不代表迭代器就一劳永逸了。
使用的时候还需斟酌数组的索引。
如下图所示:
原来的同步方法获取的节点是节点的父节点,根据父节点进行对应。
然而在同步更新文件的时候,发现这样并不好处理,在不改动原代码的情况下,设计了将列表转为树型结构的方法,这样可以从根节点向下开始遍历,便于操作。
也是在牛客网身经百战,实现这个难度不大。但在编写相关实现的时候,遇到了一个小问题。
第一步,将列表中的根节点找出来。
@Override public ClusterNode listToTree(List<ClusterNode> clusterNodes) { logger.debug("声明根节点名称"); final String ROOT_NAME = "ROOT"; logger.debug("声明根节点"); ClusterNode rootNode = null; logger.debug("获取迭代器,遍历节点列表"); Iterator<ClusterNode> 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<ClusterNode> clusterNodes) { logger.debug("清空原集合"); parentNode.getClusterNodes().clear(); logger.debug("遍历列表"); Iterator<ClusterNode> 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. * * <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>. * * @return an iterator over the elements in this list in proper sequence */ public Iterator<E> iterator() { return new Itr(); }
迭代器方法内部就返回了一个 Itr
的对象,这是 ArrayList
中的私有内部类,为什么要用内部类呢?一大好处就是内部类可以直接访问外部类的私有成员,具体进行集合操作的时候非常方便。
ConcurrentModificationException
,因为十分常见,所以我们常常只关注了这个异常怎么出现的,往往忽略了异常本身。
并发修改异常,最简单的场景,就是线程不安全的多线程并发场景。
在迭代器对象执行操作之前,都会执行 checkForComodification
方法,以判断当前操作下是否安全。就像下面的代码实现一样,判断当前的数量是否是我预期的数量。
如果不是,表示有人动过我的列表,抛异常,不干了。
如果是,表示没有人动过我的列表,才继续执行。
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
既然出现了问题,怎么解决呢?
那就不要在遍历的过程中再去递归修改了,等他遍历完再说。添加一个待处理的列表,遍历之后再递归,保证每次迭代都不冲突。
logger.debug("下一次需要处理的父节点"); List<ClusterNode> nextParentNodes = new ArrayList<>(); logger.debug("遍历列表"); Iterator<ClusterNode> 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); }
迭代器并非一劳永逸,保证多次修改的独立才是最佳实践。