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

时间:2014-07-10 12:42:48

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

9 个答案:

答案 0 :(得分:198)

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

首先,请注意您始终可以从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)这一简单事实可能不是他们想要的形式,然后呼叫者无论如何都要复制它。如果你返回一个流,他们可以collect(toCollection(factory))并以他们想要的形式获得它。

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

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

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

答案 1 :(得分:62)

我有几点要添加到Brian Goetz' excellent answer

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

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

答案 2 :(得分:2)

这个答案在Brian Goetz的评论中被拒绝了,所以最好按照其他答案。

在团队/企业/可重用类上下文中:

如果你返回一个Stream,客户端应该将其视为可能无限的列表,或者是一个不适合内存的惰性列表。因此,需要附加文档来指示将流实现为集合是否安全,如stream.collect(toList())中所述。如果这是一个理想的代码模式,那么流应该有一些像isFinite()这样的方法来防范错误的假设。

返回流以避免客户端实现同样令人困惑,因为使用.collect()实现流的实现似乎太容易了,这意味着当服务类避免实现时,客户端类可能很想在高处执行它成本。

因此,我建议使用任何非收集方法返回仅当您希望向客户端记录结果可能无限或可能不适合实现时,或者当您转换传入流并且客户端可以假设传出流的大小与传入的大小相似或更小。

对于函数式编程而言,这些流更方便并不意味着它们应该是首选的,这意味着JDK应该扩展它的集合API以允许相同的操作,以便客户不必调用collection.stream()....

用于家庭作业,严格的个人项目或其他未重复使用的代码

从单纯的算法角度来看,当忽略代码风格,可读性或OO-Design问题时,在接受的答案中优先选择Streams是可以的。但我不知道如何在StackOverflow

上做出一般建议

答案 3 :(得分:2)

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

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

答案 4 :(得分:1)

  

流设计始终是&#34;终止&#34;在他们创建的相同表达式中?

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

注意:返回一个Stream与返回一个Iterator没有什么不同(承认具有更强的表现力)

恕我直言,最好的解决方案是封装你为什么这样做,而不是返回集合。

e.g。

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

或者如果你打算计算它们

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

答案 5 :(得分:1)

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

现在,或许这表明您不需要经过检查的例外,这是公平的,但有时它们是不可避免的。

答案 6 :(得分:0)

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

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

或以函数式:

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

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

答案 7 :(得分:0)

  

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

     

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

答案 8 :(得分:-4)

我可能有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个方法,因此您没有太多的方法