为什么 Iterable<T> 没有提供 stream() 和 parallelStream() 方法?

271

我想知道为什么Iterable接口没有提供stream()parallelStream()方法。考虑以下类:

public class Hand implements Iterable<Card> {
    private final List<Card> list = new ArrayList<>();
    private final int capacity;

    //...

    @Override
    public Iterator<Card> iterator() {
        return list.iterator();
    }
}

这是一个实现了“手牌”概念的类,可以在玩集换式卡片游戏时使用。它基本上包装了一个“Card”列表,并确保最大容量并提供一些其他有用的功能。与直接实现“List”相比,这种实现更好。为了方便起见,我认为实现“Iterable”会很不错,这样你就可以使用增强型for循环来遍历它。(我的“Hand”类还提供了一个“get(int index)”方法,因此在我看来,“Iterable”是合理的。)“Iterable”接口提供以下内容(省略了Javadoc):
public interface Iterable<T> {
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

现在你可以使用以下方式获取数据流:

Stream<Hand> stream = StreamSupport.stream(hand.spliterator(), false);

那么回到实际问题:

  • 为什么 Iterable<T> 没有提供默认方法来实现 stream()parallelStream(),我没有发现任何理由说明这是不可能或者不需要的?

我找到了一个相关的问题:为什么 Stream<T> 没有实现 Iterable<T>?
奇怪的是,这个问题建议以某种其他方式进行。


1
我猜这是一个适合Lambda邮件列表的好问题。 - Edwin Dalorzo
为什么想要迭代流会很奇怪?你还有其他方法可以中断迭代吗?(好吧,Stream.findFirst() 可能是一个解决方案,但可能不能满足所有需求...) - glglgl
请参考使用Java 8 JDK将Iterable转换为Stream的实用方法获取实际解决方案。 - Vadzim
3个回答

327

这不是遗漏;2013年6月,在EG列表上进行了详细讨论。专家组的决定在这个线程中得到了明确的阐述。

虽然“stream()”方法对于“Iterable”似乎很“显而易见”(即使最初是专家小组这样认为的),但由于“Iterable”太过于一般化,因此这成为了一个问题。这是因为显而易见的方法签名:

Stream<T> stream()

并不总是你想要的。例如,一些Iterable<Integer>更愿意它们的stream方法返回一个IntStream。但是,将stream()方法放在这个层次结构的高层会使这种情况变得不可能。因此,我们通过提供一个spliterator()方法,使从Iterable创建Stream变得非常容易。在Collection中,stream()的实现只是:

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}
任何客户端都可以从 Iterable 中获取他们想要的流(stream):
Stream s = StreamSupport.stream(iter.spliterator(), false);

最终我们得出结论,将 stream() 添加到 Iterable 中会是一个错误。


8
谢谢您回答问题,我明白了。但我还是很好奇一个 Iterable<Integer> (我认为您在谈论它?)为什么要返回一个 IntStream。那么这个可迭代对象是否更应该是一个 PrimitiveIterator.OfInt 呢?或者您指的是另一种用例呢? - skiwi
153
我觉得很奇怪,似乎上述的逻辑被应用于 Iterable(我不能使用 stream(),因为可能有人想让它返回 IntStream),但是在向 Collection 中添加完全相同的方法时并没有给予同等的思考(我可能也希望我的 Collection<Integer> 的 stream() 也返回 IntStream)。无论这个方法是同时存在还是同时不存在的,人们可能都会继续过自己的生活,但是因为它存在于一个对象上而不存在于另一个对象上,所以它就成为了一个非常明显的遗漏。 - Hakanai
7
布赖恩·麦卡彻恩(Brian McCutchon):我更能理解了。听起来人们只是厌倦争论,决定采取保守措施。 - Jonathan Locke
62
虽然这很有道理,但是为什么没有一种替代静态Stream.of(Iterable)的方法,至少这样做可以通过阅读API文档相对容易地发现该方法——作为一个从未真正涉足流内部的人,我甚至从未看过被描述为提供“低级操作”,主要面向库编写者的StreamSupport - Jules
16
我完全同意Jules的观点。应该添加一个静态方法Stream.of(Iteratable iter)或Stream.of(Iterator iter),而不是使用StreamSupport.stream(iter.spliterator(), false)。 - user_3380739
显示剩余8条评论

25

我在几个Lambda项目邮件列表中进行了调查,发现一些有趣的讨论。

到目前为止,我还没有找到令人满意的解释。在阅读完所有这些内容后,我得出结论,这只是一种遗漏。但是您可以在此处看到,在设计API期间,多年来已经多次讨论过这个问题。

Lambda Libs规范专家

我在 Lambda Libs规范专家邮件列表中找到了一些关于这个问题的讨论:

Iterable/Iterator.stream()下面,Sam Pullara说:

我正在与Brian一起探讨如何实现limit/substream功能[1],他建议转换为迭代器是正确的方法。我也考虑过这个解决方案,但没有找到任何明显的方法将迭代器变成流。结果发现它已经在那里了,你只需要先将迭代器转换为分裂器,然后再将分裂器转换为流。所以这就让我们重新审视是否应该直接挂在Iterable/Iterator之一或两者都挂。

我的建议是至少在Iterator上拥有它,这样你可以在两个世界之间轻松移动,而且它也很容易被发现,而不必执行:

Streams.stream(Spliterators.spliteratorUnknownSize(iterator,Spliterator.ORDERED))

然后Brian Goetz做出了回应:

我认为Sam的观点是,有很多库类提供了Iterator,但并不一定让你自己编写spliterator。所以你只能调用stream(spliteratorUnknownSize(iterator))。Sam建议我们定义Iterator.stream()来帮助你完成这个过程。 我希望保持stream()和spliterator()方法针对库编写者/高级用户。 后来
“考虑到编写Spliterator比编写Iterator更容易,我更愿意只编写Spliterator而不是Iterator(Iterator已经过时了 :)” 不过你没有理解重点。有大量的类会直接提供一个Iterator,其中很多并不支持spliterator。 Lambda邮件列表中的以前讨论 这可能不是您想要的答案,但在Project Lambda邮件列表中曾简要讨论过此事。也许这有助于促进更广泛的讨论。
引用Brian Goetz在从Iterable流式处理中的话:
回到之前的话题...
有许多方法可以创建一个Stream。如果您对如何描述元素具有更多信息,则流库可以为您提供更多功能和性能。按照信息少到多的顺序,它们是:
迭代器
迭代器+大小
分裂迭代器
知道其大小的分裂迭代器
知道其大小并进一步知道所有子拆分都知道其大小的分裂迭代器。
(某些人可能会惊讶地发现,在Q(每个元素的工作量)非平凡的情况下,我们甚至可以从愚蠢的迭代器中提取并行性。)
如果Iterable有一个stream()方法,它将只用没有大小信息的Spliterator包装一个Iterator。但是,大多数可迭代的东西确实具有大小信息。这意味着我们正在提供不足的流。这不太好。
Stephen在这里概述的API实践的一个缺点是,接受Iterable而不是Collection,这样您就会强制通过“小管道”,因此在可能有用的情况下丢弃大小信息。如果您只想forEach它,那么这很好,但是如果您想做更多事情,最好保留所有所需的信息。
Iterable提供的默认值确实很差-尽管绝大多数可迭代对象都知道该信息,但它会丢弃大小。

矛盾吗?

虽然讨论似乎是基于Expert Group对最初基于迭代器的Streams设计所做的更改。

即便如此,在像Collection这样的接口中,stream方法的定义为:

default Stream<E> stream() {
   return StreamSupport.stream(spliterator(), false);
}

这段代码可能与Iterable接口中使用的完全相同。因此,这就是为什么我说这个答案可能不太令人满意,但对于讨论仍然很有趣。

重构证据

继续分析邮件列表,看起来splitIterator方法最初在Collection接口中,然后在2013年的某个时候将其上移至Iterable。

将splitIterator从Collection上移到Iterable

结论/理论?

那么,在Iterable中缺少该方法只是一个遗漏,因为当他们将splitIterator从Collection上移到Iterable时,似乎也应该将stream方法移动。

如果有其他原因,则不明显。还有其他理论吗?


感谢您的回复,但我不同意那里的推理。一旦您覆盖Iterablespliterator(),那么所有问题都得到解决,并且您可以轻松实现stream()parallelStream() - skiwi
@skiwi,这就是我说这可能不是答案的原因。我只是试图为讨论增添一些内容,因为很难知道专家小组为什么会做出这样的决定。我猜我们能做的就是在邮件列表中进行一些取证,看看是否能找到任何原因。 - Edwin Dalorzo
1
@skiwi 我查看了其他邮件列表,找到了更多关于讨论的证据,也许有一些想法可以帮助理论化一些诊断。 - Edwin Dalorzo
感谢您的努力,我真的应该学习如何高效地分割这些邮件列表。如果它们可以以某种现代方式进行可视化,比如论坛或其他什么,那将会很有帮助,因为阅读带有引用的纯文本电子邮件并不是特别高效。 - skiwi

6
如果您知道大小,可以使用java.util.Collection提供的stream()方法:
public class Hand extends AbstractCollection<Card> {
   private final List<Card> list = new ArrayList<>();
   private final int capacity;

   //...

   @Override
   public Iterator<Card> iterator() {
       return list.iterator();
   }

   @Override
   public int size() {
      return list.size();
   }
}

然后:

new Hand().stream().map(...)

我曾经遇到同样的问题,惊讶地发现我的Iterable实现可以通过添加size()方法(幸运的是,我有集合的大小:-)轻松地扩展为AbstractCollection实现。

您还应该考虑重写Spliterator<E> spliterator()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接