关于Java:是一个”无限”迭代器坏设计?

Is an “infinite” iterator bad design?

提供"无限"的Iterator实现通常被认为是不好的做法;即,对hasNext()的调用总是返回true?

通常我会说"是",因为调用代码的行为可能不稳定,但在下面的实现中,除非调用方从迭代器初始化的列表中删除所有元素,否则hasNext()将返回true;即,存在终止条件。你认为这是对Iterator的合法使用吗?这似乎并没有违反合同,尽管我想有人会说这是不具有约束力的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CyclicIterator<T> implements Iterator<T> {
  private final List<T> l;
  private Iterator<T> it;

  public CyclicIterator<T>(List<T> l) {
    this.l = l;
    this.it = l.iterator();
  }

  public boolean hasNext() {
    return !l.isEmpty();
  }

  public T next() {
    T ret;

    if (!hasNext()) {
      throw new NoSuchElementException();
    } else if (it.hasNext()) {
      ret = it.next();
    } else {
      it = l.iterator();
      ret = it.next();
    }

    return ret;
  }

  public void remove() {
    it.remove();
  }
}

(学究式)编辑

一些人评论了如何使用Iterator从无界序列(如斐波那契序列)生成值。然而,JavaEDCOX1 0文档说明迭代器是:

An iterator over a collection.

现在你可以说Fibonacci序列是一个无限的集合,但是在Java中,我将集合等同于EDCOX1的6接口,它提供了诸如EDCX1(7)的方法,这意味着集合必须是有界的。因此,使用Iterator作为无界序列值的生成器是否合法?


我认为这是完全合法的-一个Iterator只是一个"东西流"。为什么流必须是有界的?

许多其他语言(例如scala)都有内置无边界流的概念,并且可以迭代这些流。例如,使用scalaz

1
2
3
4
5
scala> val fibs = (0, 1).iterate[Stream](t2 => t2._2 -> (t2._1 + t2._2)).map(_._1).iterator
fibs: Iterator[Int] = non-empty iterator

scala> fibs.take(10).mkString(",") //first 10 fibonnacci numbers
res0: String = 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

编辑:就最小惊喜原则而言,我认为这完全取决于上下文。例如,我希望这个方法返回什么?

1
public Iterator<Integer> fibonacciSequence();


一个Iterator的关键是它是懒惰的,也就是说它只给你想要的东西。如果用户要求一个无限的Iterator的所有对象,那是他们的问题,而不是你的问题。


这可能是语义,但是迭代器应该努力返回下一个项目,而不考虑它何时结束。结尾是一个集合的副作用,尽管它看起来像一个常见的副作用。

不过,无穷大和拥有10万亿个项目的集合有什么区别?调用者要么想要全部,要么不想要。让调用者决定返回多少项或何时结束。

我不会说调用者不能使用for-each构造。他可以,只要他想要所有的东西。

在集合的文档中,"可能是无限集合"之类的内容是合适的,但不是"无限迭代器"。


虽然我也认为这是合理的,但我想补充一点,这样一个Iterator(或者更准确地说:一个Iterable产生这样一个Iterator)将不能很好地利用增强的for循环(每个循环都是一个k.a):

1
2
3
for (Object o : myIterable) {
   ...
}

由于增强的for循环中的代码不能直接访问迭代器,因此它不能在迭代器上调用remove()

因此,要结束循环,必须执行以下操作之一:

  • 进入Iterator的内部List并直接移除物体,可能引发ConcurrentModificationException
  • break退出回路
  • "使用"Exception退出回路
  • return离开回路

除了最后一个选项之外,其他所有选项都不是留下增强for循环的最佳方法。

"正常"for循环(或任何其他循环以及显式的Iterator变量)工作正常,当然:

1
2
3
4
for (Iterator it = getIterator(); it.hasNext();) {
  Object o = it.next()
  ...
}


对。对于何时停止迭代,您只需要一个不同的条件。

无限列表的概念在像Haskell这样的懒惰的评估语言中非常常见,并导致完全不同的程序设计。


无限迭代器在创建无限数据时非常有用,例如线性递归序列(如斐波那契序列)。

所以使用这样的工具是完全可以的。


它是一种完全合法的使用——只要它被适当地记录下来。

使用名称CyclicIterator是一个好主意,因为它推断如果循环出口大小写没有正确定义,迭代器上的循环很可能是无限的。


实际上,不管javadoc api怎么说,Iterator来自Iterable,而不是Collection。后者扩展了前者,这就是为什么它也可以生成一个Iterator,但对于Iterable而不是Collection是完全合理的。事实上,甚至有一些Java类实现EDOCX1,1,而不是EDOCX1,2。

现在,至于期望……当然,如果没有一个明确的指示,无限迭代器就不能被使用。但是,有时候,期望可能是骗人的。例如,人们可能期望一个InputStream总是结束,但这并不是一个要求。同样,从这样的流中读取的Iterator返回行可能永远不会结束。


考虑您是否具有生成器函数。迭代器可能是到生成器的合理接口,也可能不是,正如您注意到的那样。

另一方面,python有可以终止的生成器。


如果您有一个需要迭代器的对象,并且恰好给它一个无限迭代器,那么所有的都应该是好的。当然,该对象不应该要求迭代器停止。这就是潜在的坏设计。

想想音乐播放器。你告诉它在"连续播放模式"下开始播放曲目,它播放曲目,直到你告诉它停止。也许你可以通过永远改变播放列表来实现这种连续播放模式…直到用户按下"停止"。在这种情况下,MusicPlayer需要迭代播放列表,可能永远是Collection。在这种情况下,你要么想要一个你已经建造的CyclicIterator,要么想要一个随机选择下一个轨道并且永不耗尽的ShuffleIterator。到目前为止,一切都很好。

MusicPlayer.play(Iterator playlist)不在乎播放列表是否结束。它既可以使用迭代器遍历曲目列表(播放到结尾,然后停止),也可以使用ShuffleIterator遍历曲目列表(永远选择随机曲目)。一切还好。

如果有人试图写MusicPlayer.playContinuously(),希望"永远"播放曲目(直到有人按下"停止"),但接受Iterator作为参数,该怎么办?这就是设计问题。显然,playContinuously()需要一个无限的Iterator,如果它接受一个普通的Iterator作为参数,那将是一个糟糕的设计。例如,它需要检查hasNext(),并处理返回false的情况,即使这种情况永远不会发生。坏的。

因此,只要客户机不依赖于这些迭代器的结尾,就实现您想要的所有无限迭代器。就像您的客户机永远依赖于迭代器一样,那么不要给它一个普通的Iterator

这应该是显而易见的,但似乎不是。


我可以想象无限迭代器的任何用途。比如,一个程序迭代另一个服务器或后台进程发送的状态消息。虽然在现实生活中,服务器最终可能会停止运行,但从程序的角度来看,它可能会一直读下去,直到被与迭代器无关的东西停止为止。

这当然是不寻常的,正如其他人所说,应该仔细记录。但我不会说这是不可接受的。