在Java中,与循环相比,使用流的优点是什么?

219

在面试中,我被问到了这个问题,但我并不确定我给出了最好的答案。我提到了可以进行并行搜索,并且空值是通过某些我记不起来的方法处理的。现在我意识到我当时想到的是 Optionals。我错过了什么?他们声称这是更好或更简洁的代码,但我不确定我同意。


考虑到它如此简洁地被回答,似乎这并不是一个太广泛的问题。


如果他们在面试中提出这个问题,很明显他们是在寻找一种方法来使其难以找到答案吗?我的意思是,你在寻找什么?我可以拆分问题并回答所有子问题,但是然后创造一个带有指向所有子问题链接的父问题...不过似乎相当愚蠢。顺便说一下,请给我一个不太广泛的问题的例子。我不知道如何只问这个问题的部分并获得有意义的答案。我可以用不同的方式问完全相同的问题。例如,我可以问“Stream有什么用途?”或者“我何时应该使用Stream而不是for循环?”或者“为什么要使用流而不是for循环?”这些都是完全相同的问题。

...还是因为有人给出了一个非常长的多点回答,所以它被认为太广泛了?坦率地说,任何知道的人都可以在几乎任何问题上进行长时间的多点讨论。例如,如果您是 JVM 的作者之一,那么关于 for 循环的讨论您可能能够一天到晚地谈论,而我们大部分人则无法做到。

“请编辑问题,将其限制为具有足够细节以确定充分答案的特定问题。避免同时询问多个不同的问题。有关更多帮助,请参见如何提问页面。”

如下所述,已经给出了一个充分的答案,证明了这个问题是具有答案并且易于提供的。


13
个人认为这是基于个人看法的。就我个人而言,我更喜欢流式编程,因为它可以使代码更易读。它允许你写出想要的而不是如何的东西。此外,使用一行代码实现惊人的事情非常酷炫。 - Arnaud Denoyelle
37
即使只有30行,也要写成一行吗?我不太喜欢过长的代码。 - user447607
2
此外,我在这里寻找的只是面试中适当的回答。这是唯一重要的“意见”。 - user447607
2
从教育的角度来看,这个问题不仅帮我避免了未来面试中的一些尴尬,@slim真的很厉害,但从工业的角度来看,这也说明了微软编程语言是如何通过抄袭Java语言来建立自己的事业的,最终Java通过抄袭对手的Lambda表达式和流来报复,让我们拭目以待Java将来会对结构体和联合做出什么样的改变 :) - ShayHaned
5
请注意,流式编程只挖掘了函数式编程的一小部分强大功能。:-/ - Thorbjørn Ravn Andersen
显示剩余8条评论
5个回答

390
有趣的是,面试问题询问了优点,但没有询问缺点,因为两者都存在。
流是一种更具声明性的风格。或者说是一种更富表现力的风格。在代码中声明您的意图可能被认为比描述实现方式更好:
 return people
     .filter( p -> p.age() < 19)
     .collect(toList());

“...非常清楚地表明您正在从列表中筛选匹配的元素,而:”
 List<Person> filtered = new ArrayList<>();
 for(Person p : people) {
     if(p.age() < 19) {
         filtered.add(p);
     }
 }
 return filtered;

说:“我正在执行一个循环”。循环的目的深深地隐藏在逻辑中。
流通常更加简洁。同样的例子展示了这一点。简洁并不总是更好,但如果你能同时简洁和表达得清楚,那就更好了。
流与函数有很强的关联。Java 8引入了lambda和函数接口,打开了一整个强大技术的玩具箱。流提供了将函数应用于对象序列的最方便和自然的方式。
流鼓励更少的可变性。这有点相关于函数式编程方面 - 使用流编写的程序往往是您不修改对象的程序类型。
流鼓励松散耦合。您的流处理代码不需要知道流的来源或其最终的终止方法。
流可以简洁地表达相当复杂的行为。例如:
 stream.filter(myfilter).findFirst();

乍一看可能会认为它过滤整个流,然后返回第一个元素。但实际上,findFirst() 驱动整个操作,因此在找到一个项目后它会有效地停止。 流提供了未来的效率提升空间。有些人对内存中的 List 或数组的单线程流进行了基准测试,并发现其速度比等效的循环要慢。这是合理的,因为涉及到更多的对象和开销。
但是流可以扩展。除了 Java 内置支持并行流操作外,还有一些使用流作为 API 的分布式 MapReduce 库,因为模型适用于此。 缺点? 性能:通过数组的 for 循环非常轻量级,无论是堆还是 CPU 使用方面。如果原始速度和内存节俭性是优先考虑的,使用流会更糟糕。 熟悉程度。世界上充满了经验丰富的过程式程序员,他们来自许多语言背景,对于循环来说是熟悉的,而流则是新颖的。在某些环境中,您希望编写那种对这种人熟悉的代码。
认知负荷。由于其声明性质和与底层发生的事情增加的抽象程度,您可能需要建立一个新的心理模型来了解代码与执行之间的关系。实际上,只有在出现问题或需要深入分析性能或微妙的错误时才需要这样做。当它“正常工作”时,它就可以正常工作。
调试器正在改进,但即使现在,在调试器中逐步执行流代码时,与等效循环相比,可能会更加困难,因为简单的循环非常接近传统调试器使用的变量和代码位置。

4
我认为公平地说,类似于流的东西正变得越来越普遍,并且现在出现在许多并不特别面向函数式编程的常用语言中。 - Casey
23
鉴于列出的优缺点,我认为流(streams)对于除了非常简单的用途(少量逻辑if/then/else,不太多的嵌套调用或lambda函数等),在非关键性能部分来说是不值得的。 - Henrik Kjus Alstad
15
@HenrikKjusAlstad,这绝对不是我想传达的要点。流是成熟、强大、表现力高且完全适用于生产级代码。 - slim
6
我并不是说我不会在生产中使用它。而是,我更倾向于使用传统的循环/条件语句等,而不是流,特别是如果结果流看起来很复杂。我相信有些情况下流比循环和条件语句更清晰易懂,但更多时候,我认为它们的效果“持平”,甚至有时还是反过来的。因此,我更倾向于采用旧方法以减少认知开销。 - Henrik Kjus Alstad
8
@lijepdam - 但是你仍然需要一些代码来表示“我正在遍历这个列表(请看循环内部了解原因)”,尽管“遍历列表”并不是该代码的核心意图。 - slim
显示剩余6条评论

35

抛开句法上的乐趣,流(Streams)旨在处理可能无限大的数据集,而数组、集合以及几乎所有实现Iterable接口的Java SE类都是完全存储在内存中的。

流的一个缺点是过滤、映射等操作不能抛出受检异常。这使得流在中间I/O操作方面成为不良选择。


19
当然,您也可以循环遍历无限的数据源。 - slim
3
但是,如果要处理的元素已经被保存在数据库中,你如何使用Streams呢?一个初级开发者可能会被诱惑将它们全部读入集合中,以便使用Streams。但那将是一场灾难。 - Lluis Martinez
4
一款好的数据库客户端库会返回类似于 Stream<Row> 的东西,或者可以编写自己的Stream实现来封装数据库结果游标操作。 - slim
2
@xxfelixxx 你可能想要就此提出一个新问题。单凭那一行代码看起来并没有问题,所以请确保包含一个 [mre],这样我们才能亲眼看到你的问题。 - VGR
2
@xxfelixxx,那行代码没有任何作用。由于从未调用该方法,因此该方法不会抛出异常。流不是空的,只是未使用。就像将循环放入方法中,然后从未调用该方法一样。 - Holger
显示剩余6条评论

11
我认为它的易用性主要体现在并行化方面。尝试使用for循环对数百万条记录进行并行迭代。我们可以使用多个CPU,但速度并不会更快。因此,越容易并行运行,就越好。而使用Stream非常简单。
我很喜欢它们提供的详细信息。了解它们实际执行的操作和产生的结果只需很短的时间,而无需了解它们如何执行。

我根本看不到任何基准! - undefined

9
  1. You realized incorrectly: parallel operations use Streams, not Optionals.

  2. You can define methods working with streams: taking them as parameters, returning them, etc. You can't define a method which takes a loop as a parameter. This allows a complicated stream operation once and using it many times. Note that Java has a drawback here: your methods have to be called as someMethod(stream) as opposed to stream's own stream.someMethod(), so mixing them complicates reading: try seeing the order of operations in

    myMethod2(myMethod(stream.transform(...)).filter(...))
    

    Many other languages (C#, Kotlin, Scala, etc) allow some form of "extension methods".

  3. Even when you only need sequential operations, and don't want to reuse them, so that you could use either streams or loops, simple operations on streams may correspond to quite complex changes in the loops.


请解释:1. Optional接口不是处理链中的null的手段吗?关于3,这是有道理的,因为使用短路过滤器,该方法只会在指定出现次数时被调用。高效。使用它们可以减少编写需要测试的额外代码的需求,这是有道理的。经过审查,我不确定你在2中所说的顺序情况是什么意思。 - user447607
Optionalnull 的替代方案,但它与并行操作无关。除非你在问题中提到的“现在我意识到我在考虑 Optionals”只是在谈论 null 处理? - Alexey Romanov
我已经改变了2和3的顺序,并且将它们都扩展了一点。 - Alexey Romanov

7
你循环遍历序列(数组、集合、输入等),是因为你想对序列的元素应用一些函数。
流提供了在序列元素上组合函数并允许独立于具体情况实现大多数常见函数(例如映射、过滤、查找、排序、收集等)的能力。
因此,在大多数情况下,通过使用流表达循环任务可以使用更少的代码,即实现了可读性(readability)。

9
不仅仅是可读性的问题。你不必编写的代码就是你不必测试的代码。 - user447607
6
看起来你正在为面试提供不错的答案。 - wero
我根本看不到任何基准! - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接