我应该退回收藏品还是流?

fre*_*low 152 java collections encapsulation java-8 java-stream

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

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

    // ...

    public List < Player > getPlayers() {
        return Collections.unmodifiableList(players);
    }
}
Run Code Online (Sandbox Code Playgroud)

进一步假设所有客户端都立即迭代一次列表.也许将玩家放入JList或其他东西.客户端就不能存储到列表的引用以便稍后进行检查!

鉴于这种常见情况,我应该返回一个流吗?

public Stream < Player > getPlayers() {
    return players.stream();
}
Run Code Online (Sandbox Code Playgroud)

或者在Java中返回非惯用的流?设计的流是否始终在它们创建的同一表达式中"终止"?

Bri*_*etz 207

答案是,一如既往,"它取决于".这取决于返回的集合有多大.这取决于结果是否随时间变化,以及返回结果的一致性有多重要.这在很大程度上取决于用户如何使用答案.

首先,请注意您始终可以从Stream获取集合,反之亦然:

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

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

所以问题是,这对你的来电者更有用.

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

如果你的结果可能非常大,你可能更喜欢Stream,因为一次实现它可能没有任何价值,这样做会造成很大的堆压力.

如果所有调用者都要迭代它(搜索,过滤,聚合),你应该更喜欢Stream,因为Stream已经内置了这些内置并且不需要实现集合(特别是如果用户可能不处理整个结果.)这是一个非常常见的情况.

即使您知道用户将多次迭代它或以其他方式保留它,您仍然可能想要返回一个Stream,因为简单的事实是您选择将其放入的任何Collection(例如,ArrayList)可能不是他们想要的形式,然后调用者无论如何都要复制它.如果你返回一个流,他们可以做到collect(toCollection(factory))并以他们想要的形式获得它.

上述"偏好流"案例主要源于Stream更灵活的事实; 您可以延迟绑定到如何使用它,而不会产生将其具体化为集合的成本和约束.

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

所以,我要说的是,大部分的时间,流是正确的答案 - 这是更灵活,它不会强加通常,不必要的物化成本,如果需要,可以很容易地变成您所选择的集合.但有时,你可能必须返回一个集合(比如,由于强一致性要求),或者您可能需要返回集合,因为你知道用户将如何使用它,并且知道这对他们来说是最方便的事情.

  • @Marko即使你如此狭隘地限制你的问题,我仍然不同意你的结论.也许你假设创建一个Stream比用一个不可变包装器包装集合要贵得多?(而且,即使你不这样做,你在包装器上获得的流视图比你从原始文件中得到的更糟;因为UnmodifiableList不会覆盖spliterator(),你实际上会失去所有的并行性.)底线:当心熟悉偏见; 你已经知道Collection多年了,这可能会让你不信任新人. (8认同)
  • 就像我说的那样,有一些情况下它不会飞,例如当你想要在移动目标的时间内返回快照时,尤其是当你有一个强大的一致性要求时.但大多数时候,Stream似乎是更普遍的选择,除非你知道如何使用它. (6认同)
  • @MarkoTopolnik当然.我的目标是解决一般的API设计问题,这个问题正在变成FAQ.关于成本,请注意,如果您*尚未*具有物化集合,您可以返回或换行(OP确实,但通常没有),在getter方法中实现集合并不比返回流更便宜让调用者实现一个(当然,如果调用者不需要它,或者如果你返回ArrayList但是调用者想要TreeSet,那么早期实现可能要贵得多.)但Stream是新的,人们通常认为它更多$$ $比它. (5认同)
  • @MarkoTopolnik虽然内存中是一个非常重要的用例,但也有一些其他情况具有良好的并行化支持,例如非有序生成的流(例如,Stream.generate).但是,Streams不适合的情况是反应用例,其中数据以随机延迟到达.为此,我建议RxJava. (4认同)
  • @MarkoTopolnik我不认为我们不同意,除非您可能希望我们将我们的工作重点略有不同.(我们习惯了这个;不能让所有人都开心.)Streams的设计中心专注于内存数据结构; RxJava的设计中心专注于外部生成的事件.两者都是好的图书馆; 当你试图将它们应用到远离设计中心的情况时,两者都不会很好.但仅仅因为锤子是针尖的可怕工具,这并不意味着锤子有任何问题. (4认同)
  • @kevinarpe它可以,但我通常静态导入收藏家.*. (3认同)
  • 如果,正如问题所述,目标是返回已经存在于堆上的集合的只读视图,则Stream的所有优点都不适用,并且如果调用者需要Stream提供的服务,则只需一次调用.因此对于这个特定的问题,没有太多的流类型的getter. (2认同)
  • 你失去了最初的分裂者,你提出了一个强点:我会直观地假设不可变包装器暴露了底层集合的分裂器,但我敢打赌,这是一个微妙的捕获,排除了这一点.关于昂贵,我假设从收集到流是便宜的,但反过来不是(它在时间和空间都是O(n)).第三,你显然在这里给出了一个非常有教育意义和广泛有用的答案,但我仍然认为必须有一些空间来解决OP的具体问题. (2认同)
  • 也许我们在这里不同意,但这个限制是一个重要的考虑因素.根据我使用商业软件的经验,懒惰物化流的主要示例是由I/O支持的那些,这意味着它们属于"随机延迟"类别.由I/O支持的流通常也是并行化的最关键目标,因为它们通常很大并且需要重量级处理.所有这些意味着并行延迟流的最关键应用领域正是不受Streams API支持的领域. (2认同)
  • @MarkoTopolnik我想你可能有点幸运,发现了一些"足够接近"的东西.我们肯定可以采取一些措施来改进它,但最终会遇到不匹配的问题,这些错配太难以尝试和解决.Rx很棒; 用它来擅长什么.溪流很棒; 用它来擅长什么. (2认同)
  • @AskarKalykov只有当`stream()`方法没有正确的规范时才会出现这种情况!幸运的是,我们有方法来声明这样的事情的工具,它只是a)人们不写文档和b)人们不读它们.但这不是问题的方法...... (2认同)
  • 同意所有给出的“流可能更好”的原因。然而,我大部分时间仍然更喜欢收藏有一个重要原因。流使我的代码很难调试。尝试用调试器步进它......你真的不能。仅此一点,对我来说通常是默认使用集合的充分理由。然后......只有在我有足够好的和令人信服的理由的情况下才故意选择使用流,在这种特定情况下。 (2认同)
  • @AntKutschera 事实上,没有。返回 Stream 的方法总是返回一个新鲜的、非共享的流;这很容易确保,因为共享流没有什么意义。获取流的任何人(无论是他们自己创建它,还是从其他方法获取它)都有责任使用它一次。一般来说,只要您了解有关流如何工作的最基本事实,这也不是很难确保。 (2认同)

Stu*_*rks 63

我有几点要补充Brian Goetz的优秀答案.

从"getter"样式方法调用返回Stream是很常见的.请参阅Java 8 javadoc中的Stream usage页面,并查找除了之外的软件包的"返回Stream的方法" java.util.Stream.这些方法通常位于表示或可包含多个值或某些聚合的类上.在这种情况下,API通常会返回集合或它们的数组.由于Brian在他的回答中提到的所有原因,在这里添加流返回方法非常灵活.其中许多类已经具有集合或数组返回方法,因为这些类早于Streams API.如果您正在设计新的API,并且提供流返回方法是有意义的,则可能没有必要添加收集返回方法.

Brian提到了将值"物化"到集合中的成本.为了放大这一点,这里实际上有两个成本:在集合中存储值的成本(内存分配和复制)以及首先创建值的成本.通过利用Stream的懒惰行为,通常可以减少或避免后者的成本.一个很好的例子是API java.nio.file.Files:

static Stream<String>  lines(path)
static List<String>    readAllLines(path)
Run Code Online (Sandbox Code Playgroud)

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

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

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

似乎正在出现的一个习惯用法是在它表示或包含的事物的复数名称之后命名流返回方法,而没有get前缀.此外,虽然stream()只有一组可能返回的值时,while 是流返回方法的合理名称,但有时会有类具有多种类型值的聚合.例如,假设您有一些包含属性和元素的对象.您可以提供两个流返回API:

Stream<Attribute>  attributes();
Stream<Element>    elements();
Run Code Online (Sandbox Code Playgroud)

  • @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中得到了合理使用. (5认同)
  • 好点.你能详细说一下你所看到的命名成语在哪里出现,以及有多大牵引力(蒸汽?)它正在崛起?我喜欢命名约定的想法,很明显你得到了一个流与一个集合 - 虽然我也经常期望IDE完成"get"告诉我我能得到什么. (3认同)