【问题标题】:Java Iterator implementation - next() and hasNext() enforcing orderJava 迭代器实现 - next() 和 hasNext() 强制执行顺序
【发布时间】:2010-02-01 11:13:01
【问题描述】:

我有一个java.util.Iterator 的实现,它要求对next() 的调用应始终由对hasNext() 的调用进行。 (这是因为结果在多线程环境中是异步返回的,并且永远不清楚可能还有多少结果。

在 JavaDoc 中正确记录这一点是否“正确”,然后在违反时抛出 RuntimeException。或者这是否将 Iterator 接口拉伸得太远了?

感谢所有想法?

【问题讨论】:

  • 经过反思和所有出色的答案,我得出的结论是问题出在迭代器接口上。应该只有一个方法 next() 到达结束时返回 null(或毒丸)。你同意吗?
  • 我不同意。首先,null 可以是 Iterable 的有效元素。其次,考虑到泛型类型擦除,你将如何确保你的毒丸属于 T 类型,但不知何故不是有效的返回值? (如果 next() 不返回 T 类型的实例,我会立即驳回任何论点)。
  • @ILMTitan 我同意你的观点,但不要认为 Poison Pill 类型的特殊对象即使不是 T 类型也会造成任何伤害。替代方案(有两种方法)要糟糕得多.
  • Poison Pill 类型的特殊对象会造成的危害是强制 next() 的返回类型从 T 变为 Object。反过来,这将导致对 next 的每次调用都必须调用 instanceOf 后跟强制转换。在任何中等规模的项目中,这都是大量的样板代码(与泛型最初应该摆脱的样板代码完全相同。我不明白你对这两种方法解决方案的问题,这很容易理解边界检查的方法,当你做一些愚蠢的事情时抛出异常。
  • 那么按照毒丸类型,你是在说空对象模式吗?

标签: java concurrency iterator


【解决方案1】:

我可能在这里遗漏了一些东西,但为什么不在您的实现内部调用hasNext()

【讨论】:

  • @Dan 我总是试图让客户端很难以错误的方式使用 API,而且我认为在调用其他东西之前要求调用某些东西会使 API 很容易在错误的方式。
  • 不同意 - 我认为在没有先调用 hasNext() 的情况下调用 next() 是错误的。我会强制执行普遍接受的正确用法。
  • 我想你不在这里。通常可以接受的是,在大多数情况下,例程应该完成分配给它们的工作,而不依赖于调用顺序。例如,如果您对数据有一些对象并且想要计算报告,请不要说 API 的用户应该首先调用 calculateReport(),然后多次调用 getReportColumn()。相反,calculateReport 应该返回一个允许访问报告列的报告对象。这里也一样:next() 应该在不依赖特殊调用顺序的情况下工作,特别是因为它指定您可以单独调用 next()。
  • @Dan:迭代器在库中定义了一组特定的语义。任何遵守这些语义的用法都是正确的。在定义自己的迭代器时,如果您的语义与标准迭代器不同,您可能会混淆用户并使您的代码harder。虽然我只能同意正确的用法是在next() 之前检查hasNext(),但迭代器的正确响应是仅在没有更多元素时才抛出NoSuchElementException。在强制正确时,您不仅应该要求用户提供正确性,还应该要求您自己的实现提供。
  • Iterator 接口不强制要求特定的调用顺序,因此如果您的实现要求您违反接口的约定。当实例可能被传递给不了解您的实现(甚至可能早于它)的东西时,在您的实现中记录是没有用的。
【解决方案2】:

要求在next() 之前调用hasNext() 违反了iterator 合同。你真的应该重写它,如果没有要返回的元素,next() 只会抛出 NoSuchElementException

【讨论】:

  • 为什么会违反Iterator合约?
  • @Dan:因为next()只有两个有效结果:返回下一个值或在没有下一个元素时抛出NoSuchElementException。由于您会抛出异常,即使有下一个元素,您也违反了合同。
  • 同意,在这里抛出 NoSuchElementException 是正确的行为。 hasNext() 方法是避免处理异常的保护方法(如如果 hasNext() 返回 true,则 next() 永远不应抛出异常)。
  • 为了使这个答案完整,我认为它确实应该准确说明您认为违反了规范的哪一部分。
  • @JoachimSauer:我认为你误解了这个问题是关于 RuntimeExceptionNoSuchElementException,而实际上它似乎是关于投掷或不投掷。
【解决方案3】:

我想你正在做这样的事情:

class IteratorImpl<T> implements Iterator<T> {
  private Source<T> source = ...
  private T next = null;

  public boolean hasNext() {
    if(next == null) {
      next = source.poll();
    }
    return next != null;
  }

对我来说这听起来不错。我无法想象在没有hasNext 的情况下你想使用next 的情况——这将是例外情况。


编辑:

hasNext()doc 表示:

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

对我来说,实施并不违反合同。但是,我会(作为Fabian Steeg implies)仍然将next() 实现为:

  public T next() {
    if(!hasNext()) {
      throw new NoSuchElementException();
    }
    T ret = next;
    next = null;
    return ret;
  }

我的意思是,那张支票到底花了你多少钱?

您必须按照API contract 进行检查并抛出NoSuchElementException。我相信,在 !hasNext()next == null 上进行测试都将满足此标准,但我更倾向于前者。

如果有人抓到NoSuchElementException 而不是调用hasNext(),你可能会遇到更大的问题。

【讨论】:

  • 正是我想要做的 - 但 Ucleman 是否正确,它违反了迭代器合同?
  • @Dan - 根据我对文档的解释,我不认为这违反了合同。
  • 几乎我写过的每一个迭代器(很多)都用你在这里说明的那三行开始了它的 next() 实现。
  • 这是正确的答案,next() 应始终首先调用 hasNext(),但最罕见的情况除外。否则,您要么 a) 复制代码,要么 b) 在内部执行与您提供给调用者的内容不同的“hasNext”检查......从一致性的角度来看,这是危险的。
【解决方案4】:

如果您的hasNext()next() 调用不在同步块/方法中,则即使您在next() 之前调用hasNext(),也不能保证您将拥有元素。

Iterator 接口的约定是如果没有更多元素,则应该抛出NoSuchElementException。所以继续使用next() 方法,直到出现此类异常。

也就是说,看看 java.util.concurrent 包 - 它有并发集合,其迭代器可以帮助你 - 即你可以使用这些集合和迭代器而不是实现自己的。

【讨论】:

  • 也许我不清楚 - 迭代器将在单线程环境中访问 - 内部结果将在多线程环境中生成。
  • 同一个迭代器不能在两个不同的环境中使用。如果它是从多线程以任何方式访问的,那么它就是他的einvoronment是多线程的
【解决方案5】:

当没有更多元素时,我宁愿从next() 抛出异常。在多线程环境中hasNext() 无论如何也没什么用。

【讨论】:

  • 很少有迭代器被设计成线程间共享的。
  • 您不需要共享 iterator。共享底层 collection 足以使迭代器“本质上共享”。
  • 但是,正如您所写,考虑到这一点很少见。但是线程安全的迭代器肯定有实际用途。例如,使用并行消费者清除队列。当使用多个处理器/内核时,它非常有用。
【解决方案6】:

您可以自己实现Iterator 接口(如果您需要与其他API 接口)并要求您的客户实现您自己的更严格的接口。

我在我的函数式编程库中创建了这样一个类,以便在工作项目中轻松实现 Iterator 周围的 ResultSet

我的类EasierIterator实现了Iterator接口,同时要求客户端实现一个基于moveNext()/getCurrent()的更简单的接口。它实际上会为您缓存当前项目。

【讨论】:

  • 谢谢。来自枚举器接口只是 CurrentMoveNext 的 .NET,Java 迭代器接口让我发疯,试图弄清楚如何明智地处理迭代器,而你唯一能做的就是“获取下一个或 null” .我不确定 Java 是如何最终得到这个 hasNextnext 接口的,它似乎只对像内存数组这样的琐碎类型有意义,其中查看下一项是一种廉价且可重复的操作。
【解决方案7】:

您可以执行与以下类似的操作,将底层数据获取委托给私有方法,并实现 hasNext()next() 以对数据缺失做出不同的反应。这样做的好处是您可以重复调用next() 而无需先调用hasNext(),因此不会违反Iterator 的约定

public class IteratorImpl<T> implements Iterator<T> {
  private final Source<T> source;
  private T next;

  public synchronized boolean hasNext() {
    tryGetNext();
    return next != null;
  }

  public synchronized T next() {
    tryGetNext();

    if (next == null) {
      throw new NoSuchElementException();
    } 

    return next;
  }

  private void tryGetNext() {
    if (next != null) {
      next = source.poll();
    }
  }
}

【讨论】:

  • 这实际上是我最终实现的!然而,我只是在探索强制执行方法调用顺序的可能性。
  • volatile 的使用可能与问题不同步,因为它被声明为只能由单个线程访问,这里使用它还有其他原因吗?还有,在tryGetNext()中,由于操作依赖于next的值,不应该使用AtomicReferenceFieldUpdater吗?
  • 同意 - 我不知道为什么我这样写,因为它显然不是线程安全的,即使是 volatile ;有可能 2 个线程都调用 next 并且最终都调用 source.poll()。我将对其进行修改以使其完全线程安全。
【解决方案8】:

编辑:在这个答案中,我试图争辩说问题所问的内容是允许的。但是我忽略了Iterator.hasNext 文档中的一句话,这使我的整个推理无效:

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

这似乎意味着重复调用 next 直到 hasNext 返回 true 并调用 next 直到你得到 NoSuchElementException 应该返回相同的元素序列。

因此,问题所问的似乎是不允许允许的。

原答案

这是对规范律师类型答案的尝试。为了清楚起见,我将以简洁的形式重申这个问题:

Iterable 规范是否允许在调用 Iterator.next 而不事先调用 Iterator.hasNext 时抛出 NoSuchElementException,即使在先调用 Iterator.hasNext 时会返回一个元素?

讨论

Iterator.hasNext 的文档指出:

如果迭代有更多元素,则返回 true。

对于Iterator.next

抛出:NoSuchElementException - 如果迭代没有更多元素

显然,当“迭代没有更多元素”时允许抛出NoSuchElementException,但在此之前不允许。这应该与 hasNext 返回 false 的时间一致。

这就引出了一个问题:文档中的“迭代”和“元素”到底是什么意思? Iterator 文档没有给出答案,这为实施者提供了一些摆动空间。

在我看来有两种可能的解释:

  1. 从迭代器接口本身的角度来看,唯一存在的“迭代”概念是“只要hasNext 返回true”。这意味着如果客户端在hasNext 之前调用next,他们不知道是否还有更多元素,这是未定义的。

    因此,规范允许迭代器实现者决定迭代已经完成。所以这个问题的答案是肯定的。

  2. 但是Iterable.iterator 上的文档也提到了“元素”:

    返回T 类型元素的迭代器。

    那么这里的“元素”是什么意思?这是否意味着“集合中的所有元素都实现了Iterable?不,它没有这么说,而且不是所有的可迭代对象甚至都有一组固定的元素。

    对于某些特定的可迭代对象来说,“元素”的含义由实现者决定。迭代器的“元素”的有效定义可以是“集合中的所有元素,或者,在客户端决定在 hasNext 之前调用 next 之前的所有元素”。

    所以这个案例也导致了这个问题的答案是肯定的结论。 (但请看最后的注释!)

结论

文档不是很清楚,但问题的答案似乎是:是的,这是允许的。

注意

如果迭代器做了问题所要求的关于该行为的事情,当然应该记录下来。但其他可迭代对象也应该记录它们的迭代器在哪些元素上。

例如,ArrayList.iterator 文档清楚地说明了迭代器结束的哪些元素:

以正确的顺序返回此列表中元素的迭代器。


最后说明:是的,我花了这么多时间在这上面太疯狂了。

【讨论】:

  • 我认为这使情况过于复杂。 hasNext 显然是一种非变异方法,它仅查询状态“我们是否还有另一个元素”,并且“hasNext() 返回 false”和“next() throws a NoSuchElementException”都使用相同的措辞指定.如果您需要一个行为方式与问题描述相同的类,那很好,但我认为实现Iterator 是一个错误。
  • @JoachimSauer:这可能是个错误。我同意这很奇怪。在这个答案中,我尝试从规范层的角度来看待它。
  • @JoachimSauer:另一方面,next 显然是在hasNext 之前调用的,因此客户端有点执行未定义的操作,应该会出现意外行为。问题是:在实现我的迭代器时,为了支持客户端的这种奇怪行为,而不是创建一个更简单但有点奇怪的迭代器,我应该为自己制造多少额外的麻烦?
  • 我同意所有这些都是法律规定,但我会继续:是的。 hasNext 意味着要在 next 之前调用,我同意这一点(因为捕获可避免的异常是一种代码味道)。但是,仅在迭代器上使用next不是未定义的行为:如果将返回所有值,然后在下一次调用时抛出NoSuchElementException。甚至有一些阻塞的“无限”Iterator 实现总是从hasNext 返回true,并在下一个元素尚不可用时阻塞next。也很丑陋,但在技术上是有效的。
  • @JoachimSauer:嗯。我现在注意到 hasNext 文档中我之前忽略的另一句话:“换句话说,如果 next() 将返回一个元素而不是抛出异常,则返回 true。" 这实际上似乎是对您在文档中的立场的直接支持。这似乎暗示调用next 直到hasNext 返回true 并调用next 直到你得到NoSuchElementException 应该返回相同的元素序列。
猜你喜欢
  • 2017-08-21
  • 2020-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-29
  • 2020-12-30
  • 1970-01-01
  • 2015-06-25
相关资源
最近更新 更多