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

2022-08-31 05:29:43

我想知道为什么接口不提供和方法。请考虑以下类:Iterablestream()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();
    }
}

它是手牌的实现因为您可以在玩集换式纸牌游戏时手里拿着牌。

从本质上讲,它包装了一个,确保最大容量并提供一些其他有用的功能。最好直接将其实现为 .List<Card>List<Card>

现在,为了方便起见,我认为实现会很好,这样如果你想循环它,你可以使用增强的for循环。(我的类还提供了一个方法,因此在我看来这是有道理的。Iterable<Card>Handget(int index)Iterable<Card>

该接口提供以下内容(省略 javadoc):Iterable

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

这不是一个遗漏;它只是一个空洞。2013年6月对EG名单进行了详细讨论。

专家组的最终讨论植根于这一主题

虽然看起来“显而易见”(甚至在专家组看来,最初也是如此),但如此笼统的事实成为一个问题,因为明显的签名:stream()IterableIterable

Stream<T> stream()

并不总是你想要的。例如,有些东西宁愿让它们的流方法返回一个。但是,将这种方法放在层次结构中如此高的位置将使这成为不可能。因此,相反,我们通过提供一种方法使从 一个 变得非常容易。in 的实现只是:Iterable<Integer>IntStreamstream()StreamIterablespliterator()stream()Collection

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

任何客户端都可以从以下位置获取所需的流:Iterable

Stream s = StreamSupport.stream(iter.spliterator(), false);

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


答案 2

我对几个lambda邮件列表进行了调查,我想我发现了一些有趣的讨论。

到目前为止,我还没有找到令人满意的解释。在阅读了所有这些之后,我得出结论,这只是一个遗漏。但是你可以在这里看到,多年来在API的设计过程中已经讨论过几次。

Lambda Libs 规范专家

我在 Lambda Libs 规范专家邮件列表中找到了有关此内容的讨论:

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

我和Brian一起研究如何实现限制/子流功能[1],他建议转换为迭代器是正确的方法。我曾考虑过这个解决方案,但没有找到任何明显的方法来获取迭代器并将其转换为流。事实证明它在那里,你只需要首先将迭代器转换为拆分器,然后将拆分器转换为流。因此,这让我重新审视是否应该直接或两者兼而有之地挂掉这些迭代器/迭代器之一的问题。

我的建议是,至少把它放在迭代器上,这样你就可以在两个世界之间干净利落地移动,它也很容易被发现,而不必这样做:

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

然后Brian Goetz回应道

我认为 Sam 的观点是,有很多库类可以给你一个迭代器,但不要让你一定要编写自己的拆分器。所以你所能做的就是调用流(spliteratorUnknownSize(iterator))。Sam 建议我们定义 Iterator.stream() 来为你做到这一点。

我想保留 stream() 和 spliterator() 方法作为库编写器/高级用户。

以及后来的

“鉴于编写拆分器比编写迭代器更容易,我宁愿只编写拆分器而不是迭代器(迭代器是如此90年代:)”

不过,你错过了重点。那里有无数的类已经给你一个迭代器。而且其中许多还没有准备好分离器。

Lambda 邮件列表中以前的讨论

这可能不是您要寻找的答案,但在Lambda项目邮件列表中对此进行了简要讨论。也许这有助于促进关于这一主题的更广泛讨论。

用Brian Goetz的话来说,Streams from Iterable

退后一步...

创建流的方法有很多种。有关如何描述元素的信息越多,流库可以为您提供的功能和性能就越多。按照从最少到大多数信息的顺序,它们是:

迭 代

迭代器 + 大小

分路器

知道其大小的分路器

知道其大小的拆分器,并进一步知道所有子拆分器都知道其大小。

(有些人可能会惊讶地发现,在Q(每个元素的工作)不平凡的情况下,我们甚至可以从哑迭代器中提取并行性。

如果 Iterable 有一个 stream() 方法,它只会用一个 Spliterator 包装一个迭代器,没有大小信息。但是,大多数可迭代的东西都有大小信息。这意味着我们正在提供有缺陷的流。那不是很好。

Stephen 在这里概述的 API 实践的一个缺点是,接受 Iterable 而不是 Collection,这是在强制通过“小管道”来执行操作,因此在大小信息可能有用时丢弃它。如果你要做的就是 forEach,那很好,但是如果你想做更多的事情,如果你能保留所有你想要的信息,那就更好了。

Iterable提供的默认值确实是一个蹩脚的默认值 - 即使绝大多数迭代确实知道该信息,它也会丢弃大小。

矛盾?

虽然,看起来讨论是基于专家组对 Streams 的初始设计所做的更改,该设计最初基于迭代器。

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

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

这可能是在可迭代接口中使用的完全相同的代码。

所以,这就是为什么我说这个答案可能不令人满意,但仍然对讨论很有趣。

重构的证据

继续在邮件列表中进行分析,看起来splitIterator方法最初在Collection接口中,并且在2013年的某个时候,他们将其移至Iterable。

将 splitIterator 从“集合”向上拉到“可迭代”。

结论/理论?

然后,Iterable中缺少该方法很可能只是一个遗漏,因为看起来当他们将splitIterator从CollectionIal移动到Iterable时,他们也应该移动流方法。

如果有其他原因,这些原因并不明显。其他人有其他理论吗?


推荐