遍历 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("...") 方法的结果将是对“liveNodeList 类型的引用。

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 节点的主要内容,如果未能解决你的问题,请参考以下文章

markdown 遍历集合时移除元素

写一段代码在遍历 ArrayList 时移除一个元素?

javaScript之NodeList

javascript:NodeList 接口,HTMLCollection 接口

遍历节点树

js中,dom元素和节点的区别