提问人:fredoverflow 提问时间:7/10/2014 最后编辑:fredoverflow 更新时间:8/17/2021 访问量:56655
我应该返回集合还是流?
Should I return a Collection or a Stream?
问:
假设我有一个方法,将只读视图返回到成员列表中:
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 中返回流是非常规的?流是否被设计为始终在创建它们的同一表达式中“终止”?
答:
流是否被设计为始终在创建它们的同一表达式中“终止”?
这就是它们在大多数示例中的使用方式。
注意:返回 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);
}
或以 A 功能样式:
team.forEach(System.out::println);
但是,如果您想要一个更完整、更流畅的 API,流可能是一个很好的解决方案。
评论
stream.count()
我可能有 2 种方法,一种返回 a,一种将集合作为 .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 而创建列表的不可变副本。
这也只会向您的 API 添加 1 个方法,因此您没有太多方法
评论
答案是,一如既往,“视情况而定”。这取决于返回的集合有多大。这取决于结果是否随时间变化,以及返回结果的一致性有多重要。这在很大程度上取决于用户可能如何使用答案。
首先,请注意,您始终可以从 a 中获取 ,反之亦然:Collection
Stream
// 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
必须返回 a 的一种情况是,当存在强烈的一致性要求时,并且必须生成移动目标的一致快照。然后,您需要将元素放入一个不会更改的集合中。Collection
所以我想说,大多数时候,是正确的答案——它更灵活,它不会施加通常不必要的物化成本,并且如果需要,可以很容易地变成你选择的集合。但有时,您可能必须返回一个(例如,由于强一致性要求),或者您可能想要返回,因为您知道用户将如何使用它并且知道这对他们来说是最方便的事情。Stream
Collection
Collection
如果你已经有一个合适的“躺着”,并且你的用户似乎更愿意与它进行交互,那么只返回你所拥有的是一个合理的选择(尽管不是唯一的,而且更脆弱)。Collection
Collection
评论
对于Brian Goetz的出色回答,我有几点要补充。
从“getter”样式方法调用返回 Stream 是很常见的。请参阅 Java 8 javadoc 中的 Stream 用法页面,并查找 “methods...that return Stream“ 用于除 .这些方法通常位于表示或可以包含某物的多个值或聚合的类上。在这种情况下,API 通常具有返回的集合或数组。由于 Brian 在他的回答中指出的所有原因,在这里添加 Stream-return 方法非常灵活。其中许多类已经具有集合或数组返回方法,因为这些类早于 Streams API。如果要设计新的 API,并且提供 Stream-return 方法很有意义,则可能没有必要添加集合返回方法。java.util.Stream
Brian 提到了将值“具体化”到集合中的成本。为了放大这一点,这里实际上有两个成本:在集合中存储值的成本(内存分配和复制)以及首先创建值的成本。后一种成本通常可以通过利用 Stream 的懒惰寻求行为来降低或避免。一个很好的例子是 API 在:java.nio.file.Files
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());
}
当然,如果调用方过滤流以仅返回与模式匹配的行等,则可以节省大量内存空间。
一个似乎正在出现的习惯是,在它所表示或包含的事物名称的复数形式之后命名流返回方法,而不带前缀。此外,当只有一组可能的值要返回时,虽然它是流返回方法的合理名称,但有时有些类具有多种类型值的聚合。例如,假设您有一个同时包含属性和元素的对象。您可以提供两个流返回 API:get
stream()
Stream<Attribute> attributes();
Stream<Element> elements();
评论
也许 Stream 工厂会是更好的选择。只有的大赢家 通过 Stream 公开集合是因为它更好地封装了您的 域模型的数据结构。任何对域类的使用都不可能简单地影响 List 或 Set 的内部工作 通过公开 Stream。
它还鼓励域类的用户 以更现代的 Java 8 风格编写代码。这是可能的 通过保留现有的 getter 以增量方式重构到此样式 并添加新的 Stream-return getter。随着时间的流逝,您可以重写 您的旧代码,直到您最终删除了所有返回的 getter 列表或集合。一旦你有了这种重构,感觉真的很好 清除了所有遗留代码!
评论
如果流是有限的,并且对返回的对象进行了预期/正常操作,该操作将引发已检查的异常,则我总是返回一个 Collection。因为如果你要对每个可能引发检查异常的对象做一些事情,你会讨厌这个流。流的真正缺乏,我无法优雅地处理选中的异常。
现在,也许这表明您不需要检查异常,这是公平的,但有时它们是不可避免的。
与集合相比,流具有其他特征。任何方法返回的流可能是:
这些差异也存在于集合中,但它们是明显合同的一部分:
- 所有集合都有大小,迭代器/迭代可以是无限的。
- 集合是显式排序的或非排序的
- 值得庆幸的是,除了线程安全之外,并行性并不是集合所关心的
- 集合通常也是不可关闭的,因此也无需担心使用 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)
这使得 Stream 仅作为方法签名中的有效选择,当上述问题都无关紧要时,通常是当流生产者和使用者位于同一代码库中,并且所有使用者都是已知的(例如,不是可在许多地方重用的类的公共接口的一部分)。
在具有显式约定的方法签名中使用其他数据类型(并且不涉及隐式线程池处理)要安全得多,这样就不可能意外地处理具有关于有序性、大小或并行性(以及线程池使用情况)的错误假设的数据。
评论
虽然一些比较知名的受访者给出了很好的一般性建议,但令我惊讶的是,没有人明确表示:
如果您手头已经有一个“具体化”(即,它在调用之前已经创建过 - 就像给定示例中的情况一样,它是一个成员字段),那么将其转换为 .呼叫者可以很容易地自己做到这一点。然而,如果调用方想要以原始形式使用数据,则将其转换为 强制他们执行冗余工作以重新具体化原始结构的副本。Collection
Stream
Stream
评论
上一个:make_unique完美转发
评论
players.stream()
Collection
addAll