我应该返回收藏集还是流?


163

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

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中返回流非惯用语?流是否设计为始终在创建它们的相同表达式内被“终止”?


12
毫无疑问,这是成语。毕竟,players.stream()就是这样一种将流返回给调用者的方法。真正的问题是,您是否真的想将调用者限制为单遍历,并且还拒绝他通过CollectionAPI 访问您的集合?也许呼叫者只是想将addAll其转移到另一个集合?
Marko Topolnik

2
一切都取决于。您始终可以执行collection.stream()和Stream.collect()。因此,取决于您和使用该功能的呼叫者。
Raja Anbazhagan

Answers:


222

答案是一如既往的“取决于”。这取决于返回的集合的大小。这取决于结果是否随时间变化,以及返回结果的一致性有多重要。这很大程度上取决于用户使用答案的方式。

首先,请注意,您始终可以从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,原因很简单,无论您选择放入哪个Collection(例如ArrayList)都可能不是表单,然后调用者无论如何都必须复制它。如果您返回流,则他们可以collect(toCollection(factory))按照所需的形式进行获取。

上面的“首选Stream”案例主要是因为Stream更加灵活。您可以后期绑定到您的使用方式,而不会产生将其具体化为Collection的成本和约束。

当有很强的一致性要求时,必须返回集合的一种情况是,必须为移动目标生成一致的快照。然后,您需要将元素放入不会更改的集合中。

因此,我想说,在大多数情况下,Stream是正确的答案-它更加灵活,它不会带来通常不必要的实现成本,并且可以根据需要轻松地转换为您选择的Collection。但是有时,您可能必须返回Collection(例如,由于强烈的一致性要求),或者您可能想返回Collection,因为您知道用户将如何使用它,并且这对他们来说是最方便的事情。


6
就像我说的那样,在某些情况下它不会运行,例如,当您想在移动目标时返回快照时,尤其是在您有很强的一致性要求时。但是大多数时候,Stream似乎是更通用的选择,除非您对如何使用它有所了解。
Brian Goetz 2014年

8
@Marko即使您的问题范围如此狭窄,我仍然不同意您的结论。也许您以为创建Stream会比使用不可变的包装器包装集合要贵得多?(而且,即使您不这样做,在包装器上获得的流视图也比从原始视图获得的流视图更糟糕;因为UnmodifiableList不会覆盖spliterator(),您实际上将失去所有并行性。)底线:当心熟悉偏见;您已经了解Collection多年了,这可能会让您不信任新来者。
Brian Goetz 2014年

5
@MarkoTopolnik当然。我的目标是解决一般的API设计问题,这将成为常见问题。关于成本,请注意,如果您还没有实体化的集合,则可以返回或包装(OP可以,但通常没有),在getter方法中实现一个集合并不比返回流并让它便宜调用者实现了一个(当然,如果调用者不需要它,或者如果您返回ArrayList但调用者想要TreeSet,则早期实现的代价可能会高得多。)但是Stream是新的,人们通常认为它比$$更多它是。
Brian Goetz 2014年

4
@MarkoTopolnik虽然内存是一个非常重要的用例,但也有一些其他案例具有良好的并行化支持,例如无序生成的流(例如Stream.generate)。但是,在Streams不太适合的地方是反应性用例,其中数据以随机等待时间到达。为此,我建议使用RxJava。
Brian Goetz 2014年

4
@MarkoTopolnik我认为我们不同意,只是您可能希望我们将我们的工作重点稍有不同。(我们已经习惯了;不能让所有人满意。)Streams的设计中心专注于内存中的数据结构;RxJava的设计中心专注于外部生成的事件。两者都是很好的库;当您尝试将它们应用于设计中心以外的情况时,两者的效果也不佳。但是,仅仅因为锤子是针刺的可怕工具,所以这并不意味着锤子有任何问题。
Brian Goetz 2014年

63

我有几点要补充布莱恩·格茨的出色回答

从“ getter”风格的方法调用返回Stream是很常见的。请参阅Java 8 javadoc中的Stream用法页面,并为以外的包寻找“返回Stream的方法...” java.util.Stream。这些方法通常在表示或可以包含多个值或某些事物的集合的类上。在这种情况下,API通常返回它们的集合或数组。出于Brian在回答中指出的所有原因,在此处添加Stream返回方法非常灵活。这些类中的许多类已经具有集合或返回数组的方法,因为这些类早于Streams API。如果您正在设计一个新的API,并且提供流返回方法很有意义,那么也不必添加集合返回方法。

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());
}

当然,如果调用方过滤流以仅返回与模式匹配的行等,则可以节省大量的内存空间。

似乎正在出现的一种习惯用法是,在流表示的方法所表示或包含的事物的名称的复数形式后,以无get前缀的方式命名。同样,当stream()仅返回一组可能的值时,虽然对于流返回方法来说是一个合理的名称,但有时某些类具有多种类型的值的集合。例如,假设您有一些包含属性和元素的对象。您可能提供两个返回流的API:

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

3
好点。您能否说说命名成语在哪里出现以及它正在吸收多少牵引力(蒸汽?)?我喜欢命名约定的概念,这样可以很明显地看出您正在获取流还是集合-尽管我也经常希望IDE在“ get”上完成以告诉我可以得到什么。
约书亚·戈德堡

1
我对这种命名习语也非常感兴趣

5
@JoshuaGoldberg JDK似乎采用了这种命名习惯,尽管并非唯一。考虑:Java 8中存在CharSequence.chars()和.codePoints(),BufferedReader.lines()和Files.lines()。在Java 9中,添加了以下内容:Process.children(),NetworkInterface.addresses( ),Scanner.tokens(),Matcher.results(),java.xml.catalog.Catalog.catalogs()。添加了其他不使用此惯用语的流返回方法-想到了Scanner.findAll(),但是复数名词惯用语似乎已在JDK中得到了合理使用。
斯图尔特(Stuart Marks)

1

流是否设计为始终在创建它们的相同表达式内被“终止”?

这就是大多数示例中使用它们的方式。

注意:返回Stream与返回Iterator并没有什么不同(公认的表达能力更高)

恕我直言,最好的解决方案是封装执行此操作的原因,而不返回集合。

例如

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

或者如果您打算算他们

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

2
这个答案的问题在于,这将要求作者预见客户希望执行的每项操作,这将大大增加类上的方法数量。
dkatzel 2014年

@dkatzel这取决于最终用户是作者还是与之合作的人。如果最终用户不可知,那么您需要一个更通用的解决方案。您可能仍想限制对基础集合的访问。
彼得·劳瑞

1

如果流是有限的,并且对返回的对象有预期/正常的操作,这将引发已检查的异常,那么我总是返回Collection。因为如果您要对可能引发检查异常的每个对象执行某项操作,则您会讨厌该流。流的真正不足之处是无法优雅地处理已检查的异常。

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


1

与集合相反,流具有其他特征。任何方法返回的流可能是:

  • 有限或无限
  • 并行或顺序(具有可能影响应用程序任何其他部分的默认全局共享线程池)
  • 有序或无序

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

  • 所有集合都有大小,Iterator / Iterable可以是无限的。
  • 集合是显式排序或非排序的
  • 值得庆幸的是,并行性并不是线程安全性所关心的。

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

  • (有限,有序,顺序)
  • (有限,有序,并行)
  • (有限,无序,顺序)...

如果输入流具有破坏算法的特性,则编写方法将保护抛出IllegalArgumentException的流是困难的,因为这些属性是隐藏的。

当上述所有问题都不重要时,这只会将Stream保留为方法签名中的有效选择,这种情况很少发生。

在方法签名中使用具有显式协定(并且不涉及隐式线程池处理)的其他数据类型要安全得多,这使得不可能以错误的顺序,大小或并行性(以及线程池使用)假设来意外处理数据。


2
您对无限流的担心是没有根据的;问题是“我应该返回收藏集还是流”。如果可能进行收集,那么根据定义,结果是有限的。因此,担心您会冒无限迭代的风险,因为您可能已经返回了collection,这是没有根据的。该答案中的其余建议仅是不好的。在我看来,您遇到了一个过度使用Stream的人,并且在另一个方向上过度旋转。可以理解,但是不好的建议。
布莱恩·格茨

0

我认为这取决于您的情况。也许,如果您制作Team工具Iterable<Player>,就足够了。

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

或功能性风格:

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

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


请注意,在OP发布的代码中,玩家人数几乎没有用,除了作为估计(“ 1034玩家现在正在玩,请单击此处开始!”),这是因为您返回的是可变集合的不变视图,因此您现在获得的计数可能不等于从现在起三微秒的计数。因此,虽然返回Collection为您提供了一种“简便”的方法来计数(而且实际上stream.count()也很容易),但该数字对于除调试或估算以外的其他事情并不是很有意义。
Brian Goetz 2014年

0

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

如果您已经Collection手头有一个“物化”对象(即它已经在调用之前创建-就像给定示例中那样,它是一个成员字段),那么将其转换为a是没有意义的Stream。呼叫者可以自己轻松完成此操作。而如果调用者想使用原始格式的数据,则将其转换为Stream强制他们进行多余的工作以重新实现原始结构的副本。


-1

也许Stream工厂将是一个更好的选择。仅通过Stream公开集合的最大好处是,它可以更好地封装域模型的数据结构。仅通过公开Stream,使用域类的任何使用都不可能影响List或Set的内部工作。

它还鼓励您的域类的用户以更现代的Java 8风格编写代码。通过保留现有的吸气剂并添加新的返回流的吸气剂,可以逐步重构为这种样式。随着时间的流逝,您可以重写旧代码,直到最终删除所有返回List或Set的getter。一旦清除了所有旧代码,这种重构感觉就非常好!


7
是否有完全引用的理由?有资料吗?
Xerus

-5

我可能有2种方法,一种返回a Collection,一种将集合作为a返回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
因为他想在这两个选项之间进行选择,并询问每个选项的利弊。此外,它使每个人对这些概念都有更好的理解。
Libert Piou Piou 2014年

请不要那样做。想象一下API!
弗朗索瓦·戈蒂埃
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.