关于java:在给定迭代器的情况下使用for-each循环的惯用方法?

Idiomatic way to use for-each loop given an iterator?

当增强的for循环(FracH循环)被添加到Java时,它被用来与数组或EDCOX1(0)的目标一起工作。

1
2
3
for ( T item : /*T[] or Iterable<? extends T>*/ ) {
    //use item
}

这对于只实现一种迭代的集合类非常有用,因此具有单个iterator()方法。

但是,我发现自己非常沮丧,有时我想使用集合类中的非标准迭代器。例如,我最近试图帮助某人使用Deque作为后进先出/堆栈,但随后按FIFO顺序打印元素。我被迫这样做:

1
2
3
4
for (Iterator<T> it = myDeque.descendingIterator(); it.hasNext(); ) {
   T item = it.next();
   //use item
}

我失去了for each循环的优势。不仅仅是按键。如果不需要的话,我不喜欢公开迭代器,因为很容易犯两次调用it.next()等错误。

现在理想情况下,我认为for-each循环也应该接受一个Iterator。但事实并非如此。在这种情况下,是否有一种惯用的方法来使用for each循环?我也很乐意听到一些建议,比如使用像番石榴这样的公共图书馆。

在没有助手方法/类的情况下,我能想到的最好方法是:

1
2
3
for ( T item : new Iterable<T>() { public Iterator<T> iterator() { return myDeque.descendingIterator(); } } ) {
    //use item
}

这不值得使用。

我很想看到番石榴有像Iterables.wrap这样的习惯用法,但没有找到这样的用法。显然,我可以通过类或助手方法来滚动我自己的迭代器包装器。还有其他想法吗?

编辑:作为旁注,有人能给出一个合理的理由来解释为什么增强的for循环不应该仅仅接受Iterator?让我活在当前的设计中可能会有很长的路要走。


为什么增强的for循环不接受迭代器?

我想从各种答案中收集一些潜在的原因,来解释为什么for each循环不简单地接受迭代器。

  • 便利性:for-each循环的创建部分是为了方便执行给定集合中每个元素的操作。它没有替换迭代器显式使用的义务或意图(显然,如果要删除元素,则需要显式引用迭代器)。
  • 可读性:for each loop for ( Row r : table )的意思是"for each row"r"in table…"非常可读。看到for ( Row r : table.backwardsIterator() )会破坏可读性。
  • 透明度:如果一个对象同时是一个Iterable和一个Iterator,那么它的行为是什么?虽然很容易制定一致的规则(例如,在迭代器之前是ITerable),但是对于开发人员来说,行为将变得不那么透明。此外,还必须在编译时检查这一点。
  • 封装/范围:这是(在我看来)最重要的原因。for-each循环的设计目的是封装Iterator,并将其范围限制为循环。这使得循环以两种方式"只读":它不公开迭代器,这意味着没有任何(容易)有形的东西可以被循环改变其状态,也不能改变循环中操作数的状态(就像通过remove()直接与迭代器接口一样)。亲自传递迭代器必然意味着迭代器是公开的,这会使您丢失循环的两个"只读"属性。

  • 我可能会做的只是创建一个名为Deques的实用程序类,它可以支持这个,如果需要的话,还可以与其他实用程序一起使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Deques {
      private Deques() {}

      public static <T> Iterable<T> asDescendingIterable(final Deque<T> deque) {
        return new Iterable<T>() {
          public Iterator<T> iterator() {
            return deque.descendingIterator();
          }
        }
      }
    }

    这是另一个非常糟糕的情况,我们还没有lambda和方法引用。在Java 8中,您将能够编写类似这样的方法,该方法引用EDCOX1的11条匹配EDCOX1(7)的签名:

    1
    2
    Deque<String> deque = ...
    for (String s : deque::descendingIterator) { ... }


    与其创建一个descendingIterator,不如编写一个descendingIterable()方法,返回一个基于deque的降序iterable,它基本上取代了匿名类。我觉得这很合理。根据Colin的建议,此方法返回的可迭代实现在每次调用其自己的iterator()方法时,都会在原始deque上调用descendingIterator

    如果您只有一个迭代器,并且希望保持这种状态,那么您必须编写一个Iterable的实现,它包装迭代器并返回它一次,如果多次调用iterator(),则抛出异常。这是可行的,但显然是相当难看的。


    guava用户可以执行ImmutableList.copyOf(Iterator)来安全地将迭代器转换为iterable。尽管在迭代器上循环看起来很简单,但有人担心foreach会隐藏起来,最安全的选择是创建一个稳定的数据结构,如列表。

    这一点也在思想墓地中讨论:

    The biggest concern is that Iterable is generally assumed to be able to produce multiple independent iterators. The doc doesn't say this, but the Collection doc doesn't say this, either, and yet we assume it of its iterators. We have had breakages in Google when this assumption was violated.

    The simplest workaround is ImmutableList.copyOf(Iterator), which is pretty fast, safe, and provides many other advantages besides.


    Java 8(作为一种冗长的语言)的习惯用法是:

    1
    2
    3
    for (T t : (Iterable<T>) () -> myDeque.descendingIterator()) {
      // use item
    }

    也就是说,将Iterator包在Iterablelambda中。这基本上就是你自己使用匿名类所做的,但是lambda更好一些。

    当然,你可以一直使用Iterator.forEachRemaining()

    1
    2
    3
    myDeque.descendingIterator().forEachRemaining(t -> {
      // use item
    });


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class DescendingIterableDequeAdapter<T> implements Iterable<T> {
        private Deque<T> original;

        public DescendingIterableDequeAdapter(Deque<T> original) {
            this.original = original;
        }

        public Iterator<T> iterator() {
             return original.descendingIterator();
        }
    }

    然后

    1
    2
    3
    for (T item : new DescendingIterableDequeAdapter(deque)) {

    }

    因此,对于每种情况,您都需要一个特殊的适配器。我认为理论上不可能做你想做的事情,因为这个工具必须知道迭代器返回的方法存在,这样它才能调用它们。

    至于你的另外一个问题,我认为是因为for-each循环实际上是为了缩短通用场景的时间。调用一个额外的方法会使语法更加冗长。它本可以同时支持IterableIterator,但如果通过的对象同时实现了这两个功能,会怎么样?(很奇怪,但还是有可能的)。


    当然,Guava有一个针对逆向不可迭代场景的解决方案,但不幸的是,您需要两个步骤。reverse()将List作为参数,而不是Iterable

    1
    2
    3
    4
    final Iterable<String> it = Arrays.asList("a","b","c");
    for(final String item : Iterables.reverse(Lists.newArrayList(it))){
        System.out.println(item);
    }

    输出:

    c
    b
    a


    ApacheCommonsCollectionsAPI有一个名为IteratorIterable的类,用于执行以下操作:

    1
    2
    3
    4
    Iterator<X> iter;
    for (X item : new IteratorIterable(iter)) {
        ...
    }

    我建议用工厂方法创建一个助手类,您可以这样使用:

    1
    2
    3
    4
    5
    6
    7
    import static Iter.*;

    for( Element i : iter(elements) ) {
    }

    for( Element i : iter(o, Element.class) ) {
    }

    作为下一步,iter()的返回类型可以是一个流畅的接口,因此您可以执行以下操作:

    1
    2
    for( Element i : iter(elements).reverse() ) {
    }

    或者可能

    1
    2
    for( Element i : reverse(elements) ) {
    }

    您还应该了解一下op4j,它用一个非常好的API解决了许多这些问题。