共计 3431 个字符,预计需要花费 9 分钟才能阅读完成。
引言
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);
}
总结
迭代器并非一劳永逸,保证多次修改的独立才是最佳实践。