【问题标题】:Should I return a Collection or a Stream?我应该返回集合还是流?
【发布时间】:2014-08-31 20:21:21
【问题描述】:

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

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

【问题讨论】:

  • 这个成语绝对没有错。毕竟,players.stream() 就是这样一种方法,它向调用者返回一个流。真正的问题是,您真的想将调用者限制为单次遍历,并拒绝他通过Collection API 访问您的集合吗?也许调用者只是想addAll 将它发送到另一个集合?
  • 这一切都取决于。你总是可以做 collection.stream() 和 Stream.collect()。所以这取决于您和使用该功能的调用者。

标签: java collections java-8 encapsulation java-stream


【解决方案1】:

与集合相比,流具有additional characteristics。任何方法返回的流可能是:

  • 有限或infinite
  • parallel 或顺序(使用可能影响应用程序的任何其他部分的默认全局共享线程池)
  • 有序或无序
  • 是否关闭引用

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

  • 所有集合都有大小,Iterator/Iterable 可以是无限的。
  • 集合是明确排序或无序的
  • 幸运的是,除了线程安全之外,集合关心的不是并行性
  • 集合通常也是不可关闭的,因此也无需担心使用 try-with-resources 作为保护。

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

  • (有限、有序、顺序、要求关闭)
  • (有限、有序、并行、要求关闭)
  • (有限、无序、顺序、要求关闭)...

Writing method guards for streams 如果输入流具有破坏算法的特征,则抛出 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 的人,而你却在另一个方向过度旋转。可以理解,但建议不好。
【解决方案2】:

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

首先,请注意,您始终可以从 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“躺着”,并且您的用户似乎更愿意将其作为Collection与之交互,那么这是一个合理的选择(尽管不是唯一的,还有更多脆)只返回你所拥有的。

【讨论】:

  • 就像我说的,有一些情况下它不会飞,比如当你想及时返回一个移动目标的快照时,特别是当你有很强的一致性要求时。但大多数时候,Stream 似乎是更通用的选择,除非您知道具体的使用方式。
  • @Marko 即使您将问题限制得如此狭窄,我仍然不同意您的结论。也许您假设创建 Stream 比使用不可变包装器包装集合要昂贵得多? (而且,即使你不这样做,你在包装器上得到的流视图比你从原始得到的更糟糕;因为 UnmodifiableList 没有覆盖 spliterator(),你实际上将失去所有并行性。)底线:当心熟悉偏差;您已经了解 Collection 多年,这可能会让您不信任新来者。
  • @MarkoTopolnik 当然。我的目标是解决通用 API 设计问题,这已成为常见问题解答。关于成本,请注意,如果您还没有有一个物化集合,您可以返回或包装(OP 有,但通常没有),在 getter 方法中物化集合不是任何比返回一个流并让调用者实现一个流更便宜(当然,如果调用者不需要它,或者如果你返回 ArrayList 但调用者想要 TreeSet,早期实现可能会更昂贵。)但是 Stream 是新的,人们经常假设它比原来的多。
  • @MarkoTopolnik 虽然内存是一个非常重要的用例,但也有一些其他情况具有良好的并行化支持,例如无序生成流(例如 Stream.generate)。但是,Streams 不适合的地方是响应式用例,其中数据以随机延迟到达。为此,我建议使用 RxJava。
  • @MarkoTopolnik 我不认为我们不同意,只是您可能希望我们的工作重点稍有不同。 (我们已经习惯了;不能让所有人都开心。) Streams 的设计中心专注于内存数据结构; RxJava 的设计中心专注于外部生成的事件。两者都是很好的图书馆;当您尝试将它们应用到其设计中心之外的案例时,两者的表现都不是很好。但仅仅因为锤子是一种糟糕的针刺工具,并不意味着锤子有什么问题。
【解决方案3】:

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

如果您已经有一个“物化”Collection 在手(即它已经在调用之前创建 - 就像给定示例中的情况一样,它是一个成员字段),那么转换它没有意义到Stream。调用者可以自己轻松地做到这一点。然而,如果调用者想要以原始形式使用数据,则将其转换为 Stream 会强制他们执行冗余工作以重新实现原始结构的副本。

【讨论】:

  • 关于这个答案的几乎所有内容都掩盖了可疑的假设。返回集合,除非它已经是只读的或者你用只读视图包装它,这意味着调用者可以从你下面mutate集合,而流是只读的看法。您似乎认为将其“转换”为流很昂贵;它不是;它并不比包装在只读视图中贵。您似乎还假设调用者总是需要重新实现它;这种情况很少见。 (当他们这样做时,您无法保证他们想要的形式与您拥有的相同。)
  • 感谢您的评论。你完全正确,我通常认为我们会以不可修改的形式进行包装,我没有说明这一点。我认为购买流并不昂贵。我只是认为放弃原始集合的功能以支持流可能不是最佳的默认选择。返回流(当已经存在物化集合时)保留了更多的实现灵活性,但代价是如果调用者想要原始集合,则需要冗余工作+空间。而且我确实认为情况并非如此,这可能是我的错误。读者,ymmv。
【解决方案4】:

如果流是有限的,并且对返回的对象进行预期/正常操作,这将引发检查异常,我总是返回一个 Collection。因为如果你要对每个可以抛出检查异常的对象做一些事情,你会讨厌流。流的一个真正缺乏是我无法优雅地处理检查的异常。

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

【讨论】:

    【解决方案5】:

    我有几点要补充到Brian Goetz' excellent answer

    从“getter”风格的方法调用返回 Stream 是很常见的。请参阅 Java 8 javadoc 中的 Stream usage page 并为 java.util.Stream 以外的包查找“方法...返回 Stream”。这些方法通常用于表示或可以包含多个值或某物聚合的类。在这种情况下,API 通常会返回它们的集合或数组。由于 Brian 在他的回答中提到的所有原因,在这里添加 Stream-returning 方法非常灵活。其中许多类已经具有集合或数组返回方法,因为这些类早于 Streams API。如果您正在设计一个新的 API,并且提供 Stream-returning 方法是有意义的,那么可能也没有必要添加 collection-returning 方法。

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

    【讨论】:

    • 好点。你能说更多关于你在哪里看到这个命名习语出现的地方,以及它正在增加多少牵引力(蒸汽?)?我喜欢命名约定的想法,它让你得到一个流与一个集合很明显——尽管我也经常希望 IDE 在“get”上完成来告诉我我能得到什么。
    • 我对那个命名习惯也很感兴趣
    • @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 中得到了合理使用。
    【解决方案6】:

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

    它还鼓励您的域类的用户 以更现代的 Java 8 风格编写代码。有可能 通过保留现有的 getter 逐步重构为这种风格 并添加新的返回流的吸气剂。随着时间的推移,你可以重写 您的遗留代码,直到您最终删除所有返回的 getter 一个列表或集合。这种重构感觉非常好,一旦你已经 清除所有遗留代码!

    【讨论】:

    • 是否有充分引用此内容的原因?有来源吗?
    【解决方案7】:

    我认为这取决于您的情况。可能是,如果你让你的Team 实现Iterable&lt;Player&gt;,就足够了。

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

    或以函数式风格:

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

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

    【讨论】:

    • 请注意,在 OP 发布的代码中,玩家数量几乎没有用,除了作为估计('1034 玩家正在玩,点击这里开始!')这是因为你是返回可变集合的不可变视图,因此您现在获得的计数可能不等于三微秒后的计数。因此,虽然返回 Collection 为您提供了一种“简单”的计算方式(实际上,stream.count() 也很容易),但除了调试或估计之外,这个数字对于其他任何事情都没有多大意义。
    【解决方案8】:

    我可能有 2 种方法,一种返回 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 个方法,因此您没有太多方法

    【讨论】:

    • 因为他想在这两个选项之间做出选择,并询问每个选项的优缺点。此外,它使每个人都能更好地理解这些概念。
    • 请不要那样做。想象一下 API!
    【解决方案9】:

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

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

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

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

    例如

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

    或者如果您打算计算它们

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

    【讨论】:

    • 这个答案的问题是它需要作者预测客户想要做的每一个动作,这会大大增加类上的方法数量。
    • @dkatzel 这取决于最终用户是作者还是与他们一起工作的人。如果最终用户不可知,那么您需要一个更通用的解决方案。您可能仍希望限制对基础集合的访问。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-10-27
    • 2019-10-01
    • 2011-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多