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

Iterating over a two level structure using nested iterators

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

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

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

公开结构
  1. BoxIterator implements Iterator<Box>, Iterable<Box>
  2. Box implements Iterable<Drawer>
  3. DrawerIterator implements Iterator<Drawer>

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

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

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

这是否违反了 Iterator 的合同? 有没有更好的方法来使用 StAX 遍历两级结构?

Does this break the contract of Iterator?

没有

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

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

hasNext() [...] returns true if the iteration has more elements. [...] next() returns the next element in the iteration [and] throws NoSuchElementException if the iteration has no more elements.

这就是整个合同。

的确,如果基础XMLStreamReader超前,它会把你的BoxIteratorand/orDrawerIterator搞得一团糟。或者,在错误的点调用 BoxIterator.next() and/or DrawerIterator.next() 可能会搞乱迭代。但是,正确使用,例如在您上面的示例代码中,它可以正常工作并大大简化代码。您只需要记录迭代器的正确用法。

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


正如 Ivan 在评论中指出的那样,boxList 不应该是 class BoxIterator implements Iterator<Box>, Iterable<Box> 类型。你真的应该:

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

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

虽然让一个 class 同时实现 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<Box> 实例,用于迭代框列表两次。因为 boxList 可以迭代多次,所以每次迭代都需要一个新的迭代器实例。

在您的代码中:

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

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

话说回来,过早的优化是万恶之源。一个 class/object 的节省不值得可能的混乱;你应该把 BoxIterator 分成 BoxList implements Iterable<Box>BoxIterator implements Iterator<Box>.

它有可能违约,因为 hasNext() 可以 return true,但 next() 可以抛出 NoSuchElementException

hasNext()的合约是:

Returns true if the iteration has more elements. (In other words, returns true if next() would return an element rather than throwing an exception.)

但也有可能在调用 hasNext()next() 之间,另一个迭代器可能移动了流的位置,以至于不再有元素。

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

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

如果您仔细 implementing/overriding next() & hasNext() BoxIterator & DrawerIterator 中的方法,它看起来不会破坏合同通过实现 Iterator 接口。不用说,要注意的明显条件是 hasNext() 应该 return true 如果 next() 是 returning 元素,false 如果next() 给出了例外。

但是我不明白的是你为什么要BoxIterator实现Iterable<Box>

BoxIterator implements Iterator<Box>, Iterable<Box> 由于从 BoxIterable 接口覆盖 iterator() 方法总是会 return BoxIterator 的一个实例。如果你后面没有其他的objective,那么把这个特性封装在BoxIterator.

中就没有意义了

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

  1. State 对象将充当具有 popEvent 和 peekEvent 两种方法的解析器的代理。 On peek 迭代器将检查最后一个事件,但不会消耗它。在弹出时,他们将消耗最后一个事件。
  2. BoxIterable#iterator() 之后将消耗 StartElement(Boxes) 和 return 迭代器。
  3. BoxIterator#hasNext() 将查看事件并弹出它们,直到接收到 StartElement 或 EndElement。只有当接收到 StartElement(Box) 时,它才会 return 为真。
  4. BoxIterator#next() 将查看并弹出属性事件,直到收到 StartElement 或 EndElement 以初始化 Box 对象。
  5. Box#iterator() 将消耗 StartElement(Drawers) 事件,然后 return DrawerIterator.
  6. DrawerIterator#hasNext() 将偷看并弹出,直到收到 StartElement 或 EndElement。只有当它是 StartElement(Drawer)
  7. 时,它才会 return 为真
  8. DrawerIterator#next() 将消耗属性事件,直到接收到 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()
  }
}