如何使用Java 8 Stream API与org.w3c.dom.NodeList?

30
我认为接口org.w3c.dom.NodeList缺少一个stream()函数,以利用Java 8的Stream API的好处。考虑到引入默认方法以确保向后兼容性,我不明白为什么这个接口没有stream()函数。

所以我的问题是:

  • 我如何结合Stream API使用NodeList
  • 如果不鼓励这样做,原因是什么?

提前感谢!

编辑:我目前正在使用这个实用包装器:

private static Stream<Node> nodeStream(NodeList list) {
    List<Node> nodes = new ArrayList<>();

    for (int n = 0; n < list.getLength(); ++n) {
        nodes.add(list.item(n));
    }

    return nodes.stream();
}
5个回答

78

DOM 是一个奇怪的东西,API 是由 W3C 以语言无关的方式定义的,然后映射到不同的编程语言中,因此 Java 不能向核心 DOM 接口添加任何特定于 Java 的内容,除非这些内容在 DOM 规范中已经存在。

因此,虽然您不能将 NodeList 作为流使用,但是您可以很容易地从 NodeList 创建一个流,例如使用以下方法:

Stream<Node> nodeStream = IntStream.range(0, nodeList.getLength())
                                   .mapToObj(nodeList::item);
然而,有一个重要的限制条件 - DOM NodeList 是实时更新的,并反映了自创建列表以来原始DOM树的更改。如果您添加或删除DOM树中的元素,它们可能会奇迹般地出现或从现有的NodeList中消失,如果这发生在中途迭代,可能会导致奇怪的效果。如果您需要一个“死亡”节点列表,则需要将其复制到数组或列表中,就像您已经做的那样。

当更新一个节点时,是否会触发某些事件?我能否以文档更改的方式实时更新GUI中的文本? - Cardinal System

7
考虑到默认方法的引入以确保向后兼容性,我不明白为什么这个接口没有stream()函数。该接口是在Java 8之前定义的。由于在Java 8之前不存在Stream,因此NodeList不支持它。您不能直接将NodeList与Stream API结合使用。它不是“不鼓励”,而是不受支持。主要原因是历史原因。可能负责指定Java的org.w3c.dom API的人(即W3联盟)会推出一个新版本的API,更加友好于Java 8。但是,这将引入一堆新的兼容性问题。API的新版本不会与当前版本具有二进制兼容性,并且不兼容Java 8之前的JVM。然而,让W3联盟更新API比想象中复杂得多。DOM API是在CORBA IDL中定义的,Java API是通过将CORBA Java映射应用于IDL来“生成”的。这种映射由OMG指定...而不是W3联盟。因此,创建一个“Java 8 Stream友好”版本的org.w3c.dom API将涉及获取OMG更新CORBA Java映射以支持Stream(这至少从CORBA兼容性的角度来看是有问题的),或者打破Java API和CORBA之间的联系。不幸的是,了解OMG世界上关于刷新IDL到Java映射的进展(如果有)很困难...除非您在OMG成员组织工作等。

2
除非我误解了你的意思,否则你的陈述“由于在Java 8之前Stream不存在,因此NodeList无法支持它”是不准确的。问题明确要求为什么没有向NodeList添加默认方法-即类似于向Collection添加默认stream方法。 - Ian Robertson
1
就像我在回答中所说的那样,Oracle并不控制API,让OMG和/或W3C修复它并不简单。因此,这不仅仅是Oracle“修复”它的问题。 - Stephen C

4
这里有一个示例,展示如何使用流(Stream)查找特定的NodeList元素:
private String elementOfInterest;       // id of element
private String elementOfInterestValue;  // value of element

public boolean searchForElementOfInterest(Document doc)
{
        boolean bFound=false;
        NodeList nList = doc.getElementsByTagName("entity");

        // since NodeList does not have stream implemented, then use this hack
        Stream<Node> nodeStream = IntStream.range(0, nList.getLength()).mapToObj(nList::item);
        // search for element of interest in the NodeList
        if(nodeStream.parallel().filter(this::isElementOfInterest).collect(Collectors.toList()).size() > 0)
                bFound=true;

        return bFound;
}

private boolean isElementOfInterest(Node nNode)
{
        boolean bFound=false;
        assert(nNode != null);
        if (nNode.getNodeType() == Node.ELEMENT_NODE) {
                Element eElement = (Element) nNode;
                String id = eElement.getElementsByTagName("id").item(0).getTextContent();
                String data = eElement.getElementsByTagName("data").item(0).getTextContent();
                if (id.contentEquals(elementOfInterest) && data.contentEquals(elementOfInterestValue))
                        bFound = true;
        }
        return bFound;
}

请注意,要确定流中是否有一个元素与条件相匹配,最高效(且更易于阅读)的方法是在Stream类上使用anyMatch(Predicate)方法。例如,在上面的代码中,您可以简单地说:“返回nodeStream().parallel().anyMatch(this::isElementOfInterest)”。 - Ian Robertson

4

Java 8 Stream.iterate
使用方法如下:

    Stream.iterate(0, i -> i + 1)
          .limit (nodeList.getLength())
          .map (nodeList::item).forEach...

对于Java 9的迭代方法,除了之前的版本外,还有一个增强版可用:

    Stream.iterate(0, i -> i < nodeList.getLength(), i -> i + 1)
          .map (nodeList::item).forEach...

Java14中,iterate的两个版本仍然保持不变。


0

如果你想将子节点转换为仅包含节点元素的流,可以进行小的变化:

private Stream<Element> nodeLisToStreamElements(NodeList nodeList) {
    return IntStream.range(0, nodeList.getLength())
        .mapToObj(nodeList::item)
        .filter(node -> node instanceof Element)
        .map(node -> (Element) node);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接