在 return 值中用 Collection 代替 Stream 是个好主意吗?

Is it a good idea to substitute Collection for Stream in return values?

直到 Java 8,表示元素集合的 属性 通常 return 是一个集合。在没有不可变集合接口的情况下,一个常见的习惯用法是将其包装为:

Collection<Foo> getFoos(){ return Collections.unmodifiableCollection(foos); }

现在有了 Stream,很想开始公开 Streams 而不是 Collections。

我所看到的好处:

  1. 真正不变的API
  2. 大多数情况下,这种 属性 的客户端对查询或迭代结果感兴趣(如果它想对集合进行更新,那就太糟糕了..)。

另一方面,流只能使用一次,不能像常规集合那样传递。这尤其令人担忧。

这个问题与 similar looking question 不同,因为它更广泛,因为那里的 OP 明确声明他打算 return 的流不会被传递。在我看来,这方面在原始问题的答案中没有得到解决。

换句话说:在我看来,如果 API return 是一个流,一般的心态应该是所有与它的交互都必须在直接上下文中终止。应该禁止流水绕过。

但是,这似乎很难执行,除非开发人员非常熟悉 Stream API。这意味着这种 API 需要范式转变。我对这个说法是否正确?

取决于:

如果您从您的方法中执行 return 流,您 总是 需要确保它们在 returning 时尚未关闭。

在您的应用程序中使用 Streams API 将增加您的应用程序的用户也会传递 Streams 而不是 Collections 的可能性 - 这意味着他们还需要记住他们不应该 return 已经关闭了 Streams。

在私人项目中使用 Streams 可能会奏效,但如果您正在构建 public API 我认为 returning Streams 不是一个好主意。

就个人而言,我更喜欢使用 Iterables 而不是 Collections,因为它们的不可变性。我创建了一个名为 Enumerables 的包装器,以使用 Stream 具有的类似功能 API 来扩展 Iterable。

让我提出一个简单的规则:

A Stream that is passed as a method argument or returned as a method's return value must be the tail of an unterminated pipeline.

对于我们这些从事流工作的人来说,这可能是显而易见的,以至于我们从来没有费心把它写下来。但这对于第一次接触流的人来说可能并不明显,因此可能值得讨论。

Streams API package documentation 中涵盖了主要规则:一个流最多可以有一个终端操作。一旦终止,再添加任何中间或终端操作都是非法的。

另一个规则是流管道必须是线性的;他们不能有分支机构。这并没有非常清楚地记录下来,但在 Stream class documentation 大约三分之二的地方提到了它。这意味着如果中间操作或终端操作不是管道上的最后一个操作,则将其添加到流中是非法的。

大多数流方法是中间操作或终端操作。如果您尝试在已终止或不是最后一个操作的流上使用其中一个,您会通过获取 IllegalArgumentException 很快发现。这种情况偶尔会发生,但我认为一旦人们认识到管道必须是线性的,他们就会学会避免这个问题,问题就会消失。我认为这对大多数人来说很容易掌握;它不应该需要范式转变。

一旦你理解了这一点,很明显,如果你要将一个 Stream 实例传递给另一段代码——要么将它作为参数传递,要么 returning 它对调用者——它需要是流源或管道中的最后一个中间操作。也就是说,它需要是未终止管道的尾部。

To put in other words: it seems to me that if an API returns a stream, the general mindset should be that all interaction with it must terminate in the immediate context. It should be forbidden to pass the stream around.

我认为这限制太多了。只要您遵守我提出的规则,您就应该可以随心所欲地自由传递信息流。事实上,有很多用例可以从某个地方获取流、修改它并传递它。这里有几个例子。

1) 打开一个文本文件,每行包含一个 POJO 的文本表示。调用 File.lines() 获得 Stream<String>。将每一行映射到一个 POJO 实例中,并将 return 一个 Stream<POJO> 映射到调用者。调用者可能会应用筛选器或排序操作,并且 return 将流应用到其调用者。

2) 给定一个 Stream<POJO>,您可能希望有一个 Web 界面以允许用户提供一组复杂的搜索条件。 (例如,考虑一个有很多排序和过滤选项的购物网站。)您可以使用如下所示的方法代替在代码中编写复杂的大型管道:

Stream<POJO> applyCriteria(Stream<POJO>, SearchCriteria)

这将采用一个流,通过附加各种过滤器来应用搜索条件,并可能进行排序或不同的操作,然后 return 将结果流发送给调用者。

从这些例子中,我希望你能看到传递流有相当大的灵活性,只要你传递的总是未终止管道的尾部。