遍历 NodeList 时移除 DOM 节点
Posted
技术标签:
【中文标题】遍历 NodeList 时移除 DOM 节点【英文标题】:Removing DOM nodes when traversing a NodeList 【发布时间】:2010-11-25 08:04:17 【问题描述】:我即将删除 XML 文档中的某些元素,使用如下代码:
NodeList nodes = ...;
for (int i = 0; i < nodes.getLength(); i++)
Element e = (Element)nodes.item(i);
if (certain criteria involving Element e)
e.getParentNode().removeChild(e);
这会干扰 NodeList 的正确遍历吗?这种方法还有其他注意事项吗?如果这是完全错误的,那么正确的做法是什么?
【问题讨论】:
【参考方案1】:在循环时删除节点会导致不良结果,例如错过或重复的结果。这甚至不是同步和线程安全的问题,但如果节点被循环本身修改。在这种情况下,大多数 Java 的迭代器都会抛出 ConcurrentModificationException,这是 NodeList 不考虑的。
可以通过减小 NodeList 大小并同时减小迭代指针来修复它。仅当我们为每个循环迭代执行一个删除操作时,才能使用此解决方案。
NodeList nodes = ...;
for (int i = nodes.getLength() - 1; i >= 0; i--)
Element e = (Element)nodes.item(i);
if (certain criteria involving Element e)
e.getParentNode().removeChild(e);
【讨论】:
哇...有史以来最好的答案...这么多事情只是遍历 for 循环的方式!【参考方案2】:因此,鉴于在遍历 NodeList 时删除节点会导致 NodeList 被更新以反映新的现实,我假设我的索引将变得无效并且这将不起作用。
因此,似乎解决方案是在遍历期间跟踪要删除的元素,然后在不再使用 NodeList 时将它们全部删除。
NodeList nodes = ...;
Set<Element> targetElements = new HashSet<Element>();
for (int i = 0; i < nodes.getLength(); i++)
Element e = (Element)nodes.item(i);
if (certain criteria involving Element e)
targetElements.add(e);
for (Element e: targetElements)
e.getParentNode().removeChild(e);
【讨论】:
您为什么觉得有必要这样做?您的标准是否取决于元素的兄弟姐妹?如果是(换句话说,如果您需要保留兄弟姐妹),则保留一个 List(不需要 Set,不会有重复项)。 标准不依赖于兄弟姐妹,但如果我理解上面的答案,如果我删除 7 个节点中的 5 个,突然我的 NodeList 中只有 6 个节点,而我的 for 循环将有错误的索引,跳过一个节点,然后前进到列表的末尾。如果我有误解,请纠正我。 删除节点的顺序无关紧要,是吗? 好的,我明白你现在在说什么了。倒数。 啊,我明白了!所以我只需要将 for 循环更改为 for (int i = nodes.getLength() - 1; i >= 0; i--) ,然后我就不需要集合了吗?完全有道理。发布它,重新解释为什么原来的不起作用,我会把你标记为接受的答案。 :)【参考方案3】:根据 DOM 规范,调用 node.getElementsByTagName("...") 的结果应该是“实时的”,即对 DOM 树所做的任何修改将反映在 NodeList 对象中。好吧,对于符合要求的实现,那就是......
NodeList 和 NamedNodeMap 对象在 DOM 是活的;也就是说,更改为 基础文档结构是 反映在所有相关的 NodeList 和 NamedNodeMap 对象。
(DOM Specification)
因此,当您修改树结构时,符合要求的实现将更改 NodeList 以反映这些更改。
【讨论】:
那么这意味着我的索引在遍历过程中变得无效,对吧? @Dirk,除了引用有关 DOM NodeList 规范和 Java 实现它的重要信息之外......这个答案没有提供关于这个问题的结论性陈述......【参考方案4】:Practical XML 库现在包含 NodeListIterator,它包装了 NodeList 并提供完整的迭代器支持(这似乎比发布我们在 cmets 中讨论的代码更好)。如果您不想使用完整的库,请随意复制该类:http://practicalxml.svn.sourceforge.net/viewvc/practicalxml/trunk/src/main/java/net/sf/practicalxml/util/NodeListIterator.java?revision=125&view=markup
【讨论】:
【参考方案5】:根据 DOM Level 3 Core 规范,
调用node.getElementsByTagName("...")
方法的结果将是对“live”NodeList
类型的引用。
DOM 中的 NodeList 和 NamedNodeMap 对象是活动的;也就是说,对底层文档结构的更改会反映在所有相关的 NodeList 和 NamedNodeMap 对象中。 ...更改会自动反映在 NodeList 中,无需用户采取进一步行动。
1.1.1 The DOM Structure Model, para. 2
JavaSE 7 符合 DOM Level 3 规范:它实现了 live NodeList 接口并将其定义为一个类型;它在Interface Element 上定义并公开getElementsByTagName
方法,该方法返回live NodeList
类型。
参考文献
W3C - Document Object Model (DOM) Level 3 Core Specification - getElementsByTagName
JavaSE 7 - Interface Element
JavaSE 7 - NodeList Type
【讨论】:
【参考方案6】:旧帖子,但没有任何标记为答案。我的方法是从头开始迭代,即
for (int i = nodes.getLength() - 1; i >= 0; i--)
// do processing, and then
e.getParentNode().removeChild(e);
有了这个,你不必担心 NodeList 在你删除的时候会变短。
【讨论】:
【参考方案7】:如前所述,删除一个元素会减小列表的大小,但计数器仍在增加 (i++):
[element 1] <- Delete
[element 2]
[element 3]
[element 4]
[element 5]
[element 2]
[element 3] <- Delete
[element 4]
[element 5]
--
[element 2]
[element 4]
[element 5] <- Delete
--
--
[element 2]
[element 4]
--
--
--
我认为最简单的解决方案是删除循环中的 i++ 部分,并在未删除迭代元素时根据需要执行此操作。
NodeList nodes = ...;
for (int i = 0; i < nodes.getLength();)
Element e = (Element)nodes.item(i);
if (certain criteria involving Element e)
e.getParentNode().removeChild(e);
else
i++;
当迭代元素被删除时,指针停留在同一个位置。列表会自行移动。
[element 1] <- Delete
[element 2]
[element 3]
[element 4]
[element 5]
[element 2] <- Leave
[element 3]
[element 4]
[element 5]
--
[element 2]
[element 3] <- Leave
[element 4]
[element 5]
--
[element 2]
[element 3]
[element 4] <- Delete
[element 5]
--
[element 2]
[element 3]
[element 5] <- Delete
--
--
[element 2]
[element 3]
--
--
--
【讨论】:
【参考方案8】:最后,您必须更新项目路径中的 XML 文件。
TransformerFactory transFactory = TransformerFactory.newInstance();
Transformer transformer = transFactory.newTransformer();
DOMSource source = new DOMSource(documentoXml);
StreamResult result = new StreamResult(new File(path + "\\resources\\xml\\UsuariosFile.xml"));
transformer.transform(source, result);
如果你不放这些行,你的文件将不会被更新
【讨论】:
以上是关于遍历 NodeList 时移除 DOM 节点的主要内容,如果未能解决你的问题,请参考以下文章