使用嵌套迭代器迭代两级结构

Posted

技术标签:

【中文标题】使用嵌套迭代器迭代两级结构【英文标题】:Iterating over a two level structure using nested iterators 【发布时间】:2016-12-02 12:35:30 【问题描述】:

我有以下两级XML 结构。一个盒子列表,每个盒子都包含一个抽屉列表。

<Boxes>
    <Box id="0">
        <Drawers>
            <Drawer id="0"/>
            <Drawer id="1"/>
            ...
        </Drawers>
    </Box>
    <Box id="1">
...
    </Box>
</Boxes>

我正在使用StAX 解析它并通过两个Iterators 暴露结构:

    BoxIterator implements Iterator&lt;Box&gt;, Iterable&lt;Box&gt; Box implements Iterable&lt;Drawer&gt; DrawerIterator implements Iterator&lt;Drawer&gt;

然后我可以执行以下操作:

BoxIterator boxList;
for (Box box : boxList) 
  for (Drawer drawer : box) 
    drawer.getId()
  

Iterators 的底层,我使用的是StAX,它们都在访问相同的底层XMLStreamReader。如果我调用BoxIterator.next(),它将影响后续调用DrawerIterator.next() 时返回的结果,因为光标将移动到下一个框。

这会破坏Iterator 的合同吗? 有没有更好的方法来使用StAX 迭代两级结构?

【问题讨论】:

您的描述看起来像Box.iterator 返回一个新的DrawerIterator,如果是这样,合同就不会被破坏,因为DrawerIterator 无论如何都应该只返回当前框内的元素。 @Thomas Box.iterator() 将在每次调用时返回相同的DrawerIterator,因为无论如何它们都将访问相同的底层流。这意味着即使是过去调用Box.iterator() 返回的DrawerIterator 也会神奇地提前。所有人都将始终在相同的光标位置访问基础流。 啊,我明白了。那会破坏合同。您是否需要在每次调用时返回相同的实例?如果您每次都返回一个新实例并按顺序迭代(即没有随机访问),那么光标位置是否会被提前并不重要。在您遍历一个盒子的抽屉后,对该盒子的任何进一步调用 DrawerIterator 的 hasNext() 应该返回 false。 @Thomas 我想这是可能的,尽管它需要一些簿记。在实践中,我只想以理智的方式使用它,所以我不知道是否值得做预防病理病例的工作。另一方面,我想应该有更好的模式。我正在考虑只做一个DrawerIterator,它会有一个currentBox 字段,会自动更新。然而,这仍然存在一个问题,即所有迭代器都将访问相同的底层流,因此推进一个,将推进所有其他迭代器。 @Roland:你想达到什么目的?您只对Drawer ids 或Drawers 感兴趣吗?您对Box id 不感兴趣吗?也许你可以用另一种方式使用 StAX。 StAX 可以在找到元素的开头或结尾时解析文件并生成事件。您所要做的就是检查该元素是Box 还是Drawer 【参考方案1】:

您的代码的唯一设计问题是BoxIterator 实现了IteratorIterable。通常,每次调用 iterator() 方法时,Iterable 对象都会返回新的有状态 Iterator。因此,两个迭代器之间应该没有干扰,但是您需要一个状态对象来正确实现从内部循环退出(可能您已经拥有它,但为了清楚起见,我必须提到它)。

    State 对象将充当解析器的代理,具有两个方法 popEvent 和 peekEvent。偷看迭代器将检查最后一个事件,但不会消耗它。在弹出时,他们将消费最后一个事件。 BoxIterable#iterator() 将消耗 StartElement(Boxes) 并在此之后返回迭代器。 BoxIterator#hasNext() 将查看事件并弹出它们,直到收到 StartElement 或 EndElement。只有在收到 StartElement(Box) 时才会返回 true。 BoxIterator#next() 将 peek-and-pop 属性事件,直到收到 StartElement 或 EndElement 来初始化 Box 对象。 Box#iterator() 将消费 StartElement(Drawers) 事件,然后返回 DrawerIterator。 DrawerIterator#hasNext() 将在收到 StartElement 或 EndElement 之前进行偷看。只有当它是 StartElement(Drawer) 时才会返回 true DrawerIterator#next() 将使用 Attribute 事件,直到收到 EndElement(Drawer)。

您的用户代码几乎不会被修改:

BoxIterable boxList;
/*
 * boxList must be an BoxIterable, which on call to iterator() returns 
 * new BoxIterator initialized with current state of STaX parser
 */
for (Box box : boxList)  
  /* 
   * on following line new iterator is created and initialized 
   * with current state of parser 
   */
  for (Drawer drawer : box)  
    drawer.getId()
  

【讨论】:

通常,每次调用 iterator() 方法时,Iterable 对象都会返回新的有状态 Iterator。 这里不是这样。 BoxIterator 有一个底层的XMLStreamReader,所以我只在iterator() 方法中返回this。此迭代器的状态将是光标恰好位于底层流上的位置。【参考方案2】:

如果您通过实现Iterator 接口仔细实现/覆盖BoxIteratorDrawerIterator 中的next()hasNext() 方法,它看起来不会违反合同。不用说,要注意的明显条件是,如果 next() 返回一个元素,hasNext() 应该返回 true,如果 next() 给出异常,则 false 应该返回。

但我无法理解的是你为什么让BoxIterator实现Iterable&lt;Box&gt;

BoxIterator implements Iterator&lt;Box&gt;, Iterable&lt;Box&gt; 由于从Iterable 接口为Box 覆盖iterator() 方法总是会返回BoxIterator 的实例。如果你背后没有任何其他目的,那么在BoxIterator中封装这个特性是没有目的的。

【讨论】:

【参考方案3】:

这会违反Iterator的合同吗?

没有。

Java Iterator 强加了两个“合同”。第一个合约是 Java 接口本身,它声明了 3 个方法:hasNext()next()remove()。任何实现此Iterator 接口的类都必须定义这些方法。

第二个合约定义了Iterator的行为:

hasNext() [...] 如果迭代有更多元素,则返回 true。 [...] next() 返回迭代中的下一个元素 [并且] 如果迭代没有更多元素,则抛出 NoSuchElementException

这就是整个合同。

确实,如果底层XMLStreamReader 是高级的,它可能会弄乱你的BoxIterator 和/或DrawerIterator。或者,在错误的位置调用BoxIterator.next() 和/或DrawerIterator.next() 可能会打乱迭代。但是,正确使用,例如在上面的示例代码中,它可以正常工作并大大简化了代码。您只需要记录迭代器的正确用法即可。

作为一个具体的例子,Scanner 类实现了Iterator&lt;String&gt;,但还有许多其他方法可以推进底层流。如果存在由Iterator 类强加的更强大的契约,那么Scanner 类本身就会违反它。


正如 Ivan 在 cmets 中指出的那样,boxList 不应该是 class BoxIterator implements Iterator&lt;Box&gt;, Iterable&lt;Box&gt; 类型。你真的应该有:

class BoxList implements Iterable<Box>  ... 
class BoxIterator implements Iterator<Box>  ... 

BoxList boxList = ...;
for (Box box : boxList) 
  for (Drawer drawer : box) 
    drawer.getId()
  

虽然让一个类同时实现 IterableIterator 在技术上并不是错误对于您的用例,但它可能会导致混淆。

在另一个上下文中考虑这段代码:

List<Box> boxList = Arrays.asList(box1, box2, box3, box4);
for(Box box : boxList) 
    // Do something

for(Box box : boxList) 
    // Do some more stuff

在这里,boxList.iterator() 被调用了两次,以创建两个单独的 Iterator&lt;Box&gt; 实例,用于两次迭代框列表。因为boxList 可以迭代多次,每次迭代都需要一个新的迭代器实例。

在您的代码中:

BoxIterator boxList = new BoxIterator(xml_stream);
for (Box box : boxList) 
  for (Drawer drawer : box) 
    drawer.getId();
  

因为您正在对流进行迭代,所以您不能(在不倒带流或存储提取的对象的情况下)第二次迭代相同的节点。不需要第二个类/对象;同一个对象可以同时充当 Iterable 和 Iterator ...这为您节省了一个类/对象。

话虽如此,过早优化是万恶之源。一个类/对象的节省不值得可能的混淆;您应该将BoxIterator 拆分为BoxList implements Iterable&lt;Box&gt;BoxIterator implements Iterator&lt;Box&gt;

【讨论】:

其实代码样例不是那么好,因为BoxIterator类既是Iterable又是Iterator。如果迭代器的状态未重置,则第二次使用同一实例时可能会变得混乱。 @IvanGammel 你说得有道理。 BoxIterator 只是在对iterator() 的调用中返回this,并且不会重置底层XMLStreamReader 上的光标位置。所以也许我不应该使用整个 Iterator, Iterable 范式。我这样做只是为了能够使用增强的 for 循环,即语法糖。 @Roland,虽然这不是解析 XML 的常用方法,但如果输入很大并且堆限制很小,则您的用例是有效的(否则您可以使用 XMLBeans 将整个文件解析为对象模型或 XStream),因此您确实可以使用这种方法(对我来说看起来像一个 Active Record 模式)。您只需要仔细实施即可。 @Roland 代码示例for (Box box : boxList) for (Drawer drawer : box) drawer.getId(); 很好。唯一“不好”的部分是 boxList 类型为 BoxIterator。它确实应该是BoxList implements Iterable&lt;Box&gt; 类型。我会将其添加到答案中。【参考方案4】:

由于hasNext() 可以返回true,但next() 可以抛出NoSuchElementException,它有可能违反合同。

hasNext()的合约是:

如果迭代有更多元素,则返回 true。 (换句话说,如果 next() 将返回一个元素而不是抛出异常,则返回 true。)

但是在调用hasNext()next() 之间可能会发生另一个迭代器移动了流的位置以致没有更多元素的情况。

但是,以您使用它的方式(嵌套循环),您不会遇到损坏。

如果您要将迭代器传递给另一个进程,那么您可能会遇到这种损坏。

【讨论】:

您指出的问题可能发生在任何Iterator 上,不是吗?如果在调用hasNext() 之后,您将Iterator 传递给另一个使用它的进程,那么next() 将不会给您返回您期望的结果。 @Roland 我的意思是,将 another 迭代器提供给另一个进程可能会影响迭代器。调用 next() 会影响 所有 迭代器,因为它们共享相同的底层输入。 (几乎)每个迭代器都与 something 共享底层状态。即使hasNext() 返回true,它也不能保证next(),如果立即调用,将总是成功;它可能会抛出ConcurrentModificationException。迭代器只是帮手;它们通常在语法上很方便,但它们从不保证在某些结构上“不会被破坏或损坏的迭代”。 @ajn 当底层集合被外部修改时抛出ConcurrentModificationException合同的一部分。但是这里我们有一个问题,简单地调用next() 有一个意想不到的(对用户而言)修改支持对象的副作用(它不可撤销地推进流指针),并且我们有多个迭代器在相同的支持对象,所以 hasNext() 在调用 next() 时可能不准确。大多数迭代器并非如此。 不!只需阅读Java SE 8 Iterator。完全没有提到ConcurrentModificationException。绝对不是Iterator合同的一部分。

以上是关于使用嵌套迭代器迭代两级结构的主要内容,如果未能解决你的问题,请参考以下文章

使用嵌套迭代器有啥意义吗?

C++ 嵌套迭代器

使用 Meteor 时如何正确设置嵌套 #each 空格键迭代器的范围?

如何使用迭代器嵌套循环?

LeetCode 341 扁平化嵌套列表迭代器

如何通过迭代器访问向量中的嵌套对?