我应该返回集合还是流?

假设我有一个将只读视图返回到成员列表的方法:

class Team {
private List<Player> players = new ArrayList<>();


// ...


public List<Player> getPlayers() {
return Collections.unmodifiableList(players);
}
}

进一步假设客户机所做的全部工作是立即对列表进行一次迭代。也许是为了把玩家放进一个 JList 什么的。客户端做 没有存储一个参考列表,以便以后检查!

考虑到这个常见的场景,我应该返回一个流吗?

public Stream<Player> getPlayers() {
return players.stream();
}

或者在 Java 中返回流是非惯用的?流是否被设计成总是在创建它们的同一个表达式中“终止”?

52658 次浏览

流是否被设计成总是在创建它们的同一个表达式中“终止”?

这就是它们在大多数例子中的用法。

注意: 返回一个 Stream 与返回一个迭代器没有什么不同(允许的表达能力要强得多)

恕我直言,最好的解决方案是封装您这样做的原因,而不是返回集合。

例如:。

public int playerCount();
public Player player(int n);

或者你打算数一数

public int countPlayersWho(Predicate<? super Player> test);

我认为这取决于你的情况。也许,如果你让你的 Team实现 Iterable<Player>,这就足够了。

for (Player player : team) {
System.out.println(player);
}

或功能性风格:

team.forEach(System.out::println);

但是如果您想要一个更完整、更流畅的 api,流可能是一个很好的解决方案。

我可能有两个方法,一个返回 Collection,另一个返回 Stream

class Team
{
private List<Player> players = new ArrayList<>();


// ...


public List<Player> getPlayers()
{
return Collections.unmodifiableList(players);
}


public Stream<Player> getPlayerStream()
{
return players.stream();
}


}

这是两全其美的办法。客户机可以选择是要 List 还是 Stream,而不必为了获得 Stream 而额外创建对象,即创建一个 List 的不可变副本。

这也只是为你的 API 增加了1个方法,所以你没有太多的方法

答案一如既往地是“视情况而定”。这取决于返回的集合有多大。它取决于结果是否随时间而变化,以及返回结果的一致性有多重要。这在很大程度上取决于用户可能如何使用这个答案。

首先,注意你总是可以从 Stream得到 Collection,反之亦然:

// If API returns Collection, convert with stream()
getFoo().stream()...


// If API returns Stream, use collect()
Collection<T> c = getFooStream().collect(toList());

所以问题是,哪个对你的来电者更有用。

如果您的结果可能是无限的,那么只有一个选择: Stream

如果结果可能非常大,那么您可能更喜欢 Stream,因为一次性全部实现它可能没有任何价值,这样做可能会产生显著的堆压力。

如果调用者要做的只是遍历它(搜索、过滤、聚合) ,那么您应该选择 Stream,因为 Stream已经内置了这些功能,而且不需要具体化一个集合(特别是当用户可能无法处理整个结果时)这是很常见的情况。

即使你知道用户会多次迭代它或者保留它,你仍然可能想要返回一个 Stream代替,因为简单的事实是,无论你选择把它放在什么 Collection(例如,ArrayList)可能不是他们想要的形式,然后调用者必须无论如何都要复制它。如果你返回一个 Stream,他们可以做 collect(toCollection(factory)),并得到它的形式正是他们想要的。

上述“更喜欢 Stream”的情况大多源于这样一个事实,即 Stream更加灵活; 您可以后期绑定到如何使用它,而不会产生将其具体化为 Collection的成本和约束。

必须返回 Collection的一种情况是,当存在强烈的一致性要求时,必须生成移动目标的一致性快照。然后,您将希望将元素放入一个不会更改的集合中。

所以我想说,大多数情况下,Stream是正确的答案ーー它更加灵活,不会通常造成不必要的物化成本,而且如果需要的话,它可以很容易地变成你所选择的收藏品。但有时,您可能不得不返回一个 Collection(比如,由于强烈的一致性要求) ,或者您可能想返回 Collection,因为您知道用户将如何使用它,并且知道这对他们来说是最方便的事情。

如果您已经有一个合适的 Collection“闲置”,并且您的用户似乎更愿意以 Collection的形式与它交互,那么只返回您所拥有的内容是一个合理的选择(尽管不是唯一的选择,而且更脆弱)。

我有几点要加到 Brian Goetz 的绝妙回答

从“ getter”样式的方法调用返回 Stream 是很常见的。查看 Java8 javadoc 中的 流使用情况页面,并为 java.util.Stream以外的包寻找“返回 Stream 的方法”。这些方法通常位于表示或可以包含某些内容的多个值或聚合的类上。在这种情况下,API 通常会返回它们的集合或数组。出于 Brian 在答案中提到的所有原因,在这里添加 Stream-return 方法是非常灵活的。其中许多类已经具有集合或数组返回方法,因为这些类早于 Streams API。如果您正在设计一个新的 API,并且提供 Stream 返回方法是有意义的,那么可能也没有必要添加集合返回方法。

Brian 提到了将价值“具体化”到一个集合中的成本。为了进一步说明这一点,这里实际上有两个成本: 在集合中存储值的成本(内存分配和复制) ,以及首先创建值的成本。后者的成本通常可以通过利用 Stream 的懒惰寻求行为来降低或避免。java.nio.file.Files中的 API 就是一个很好的例子:

static Stream<String>  lines(path)
static List<String>    readAllLines(path)

readAllLines不仅必须在内存中保存整个文件的内容,以便将其存储到结果列表中,它还必须在返回列表之前将文件读取到最后。lines方法可以在执行一些设置之后立即返回,将文件读取和换行留到以后需要的时候——或者根本不需要。这是一个巨大的好处,例如,如果来电者只对前十行感兴趣:

try (Stream<String> lines = Files.lines(path)) {
List<String> firstTen = lines.limit(10).collect(toList());
}

当然,如果调用方对流进行过滤,只返回与模式匹配的行,那么可以节省大量的内存空间。

一个似乎正在出现的习惯用法是将流返回方法命名为它所表示或包含的事物的复数名称,而不使用 get前缀。此外,当只有一组可能的值要返回时,对于流返回方法来说,stream()是一个合理的名称,但是有时候有些类具有多种类型的值的聚合。例如,假设您有一些同时包含属性和元素的对象。您可以提供两个流返回 API:

Stream<Attribute>  attributes();
Stream<Element>    elements();

也许一个溪流工厂将是一个更好的选择 通过 Stream 公开集合是因为它更好地封装了您的 域模型的数据结构。您的域类的任何使用都不可能简单地影响 List 或 Set 的内部工作方式 通过暴露一个数据流。

它还鼓励您的域类的用户 以更现代的 Java8风格编写代码 通过保留现有的 getter 来增量重构到这种样式 并添加新的返回流的 getter。随着时间的推移,您可以重写 直到最终删除所有返回的 getter 为止 列表或集合。这种重构感觉真的很好,一旦你已经 清除了所有的遗留代码!

如果流是有限的,并且对返回的对象有一个预期的/正常的操作,这将抛出一个检查过的异常,我总是返回一个 Collection。因为如果您要对每个对象执行某些操作,从而引发检查异常,那么您将讨厌流。流的一个真正缺陷是无法优雅地处理检查过的异常。

现在,也许这是一个信号,表明您不需要检查异常,这是公平的,但有时它们是不可避免的。

与集合不同,流具有 附加特征。任何方法返回的流可能是:

  • 有限或 无穷无尽
  • 并行 或顺序(使用默认的全局共享线程池,该线程池可以影响应用程序的任何其他部分)
  • 有命令的或无命令的
  • 是否保留要关闭的引用

这些差异在收藏中也存在,但它们是显而易见的契约的一部分:

  • 所有的系列都有大小,Iterator/Iterable 可以是无限的。
  • 集合是显式有序的还是无序的
  • 值得庆幸的是,集合并不关心线程安全以外的东西
  • 集合通常也不是可关闭的,因此也不必担心使用 try-with-resources 作为保护。

作为流的使用者(无论是从方法返回还是作为方法参数) ,这是一种危险和令人困惑的情况。为了确保算法正确运行,流的消费者需要确保算法不会对流的特征做出错误的假设。这是一件非常困难的事情。在单元测试中,这意味着您必须将所有测试相乘,以使用相同的流内容进行重复,但是使用具有

  • (有限,有序,顺序,要求-关闭)
  • (有限,有序,平行,要求-关闭)
  • (有限,无序,顺序,要求-关闭) ..。

如果输入流有一个破坏算法的特征,那么编写方法保护流 ,这会抛出一个 IllegalArgumentException,这是很困难的,因为属性是隐藏的。

文档可以减轻这个问题,但是它有缺陷,而且经常被忽视,并且在修改流提供程序时没有帮助。作为一个例子,看看这些 Java8文件的 javadocs:

 /**
* [...] The returned stream encapsulates a Reader. If timely disposal of
* file system resources is required, the try-with-resources
* construct should be used to ensure that the stream's close
* method is invoked after the stream operations are completed.
*/
public static Stream<String> lines(Path path, Charset cs)
/**
* [...] no mention of closing even if this wraps the previous method
*/
public static Stream<String> lines(Path path)

这样只有当上述问题都不重要的时候,流才能作为方法签名中的有效选择,通常是当流生产者和使用者处于相同的代码库中,并且所有使用者都是已知的时候(例如,在许多地方不是可重用类的公共接口的一部分)。

在方法签名中使用其他数据类型更加安全,这些数据类型有一个明确的契约(并且不涉及隐式的线程池处理) ,这样就不可能因为错误的有序性、大小或并行性假设(以及线程池使用)而意外地处理数据。

尽管一些知名度较高的受访者给出了很好的一般性建议,但令我惊讶的是,没有人明确表示:

如果您手头已经有了一个“具体化”的 Collection(也就是说,它在调用之前就已经创建了——在给定的示例中就是这种情况,它是一个成员字段) ,那么将它转换成 Stream是没有意义的。打电话的人可以很容易地自己做到这一点。然而,如果调用方希望使用原始形式的数据,则将其转换为 Stream将迫使它们进行冗余工作,以重新实现原始结构的副本。