Stream

什么是 Stream 流

流就相当于工厂的流水线工作,传送带上是一件件产品,而传送带周围是一些工人。有的负责把挑出次品,把它放到其他另一条流水线中重新加工,有的负责根据产品颜色进行收集,等等。如下图所示。
image-20210424211146663.png

三个员工对传送带上的产品进行操作,老王负责收集把黄色产品挑出,老周负责把产品按先后排序,而老赵负责把残次品挑拣出来,拆开零件放到新的床送带中,继续加工。传送带的终点就是篮子,产品都流向篮子里面。

流的操作就是如此。在没有流之前,我们要自己从一堆产品中找到残次品很难。但是我们有了流之后,我们有了流水线,有了工人,我们就可以让工人帮我们收集,让他们来处理,我们只是充当老板的角色,下达命令即可。在 Java 的流 (Stream) 中,我们只需要使用一些命令,剩下的交给程序本身去执行。这样做既减少了代码量,又能高效完成我们的需求,真是两全其美。

在上面的过程中,我们把老王和老周,以及老赵操作叫做中间操作。因为他们并没有把产品收集起来,他们只是把其他不合格的产品过滤挑出,产品最终都是到篮子里面。但是你装产品不一定是用篮子,你也可以使用箱子,等其他容器。我们把使用这些容器装产品的操作称作终结操作。在 java 中,中间操作就是返回为 Stream 的操作,终结操作就是返回非Stream 的操作。下面会详细讲解。

Stream API

创建 Stream

  • Collection.stream()
  • Stream.of()
  • String.chars()
  • IntStream.range() 等

Stream 中间操作

  • filter 按照输入的条件,过滤流中的元素
  • map
  • sorted 对流中的元素排序

Stream 终结操作

  • forEach 遍历流中的元素
  • count / max / min 统计元素个数 、最大元素、最小元素
  • findFirst / findAny 找到第一个元素、随机找一个元素
  • anyMatch / noneMatch 是否有匹配的元素、没有匹配的元素
  • collect

从方法名中我们就能了解它的用处,更为详细的 API 操作可以查看 Stream 源代码。

Collector 操作

Collector 操作是 Stream 中最强大的操作,他就是我们上面例子中提到的「篮子」「箱子」等。它具有以下 API

  • toSet / toList / toCollection
  • joining()
  • toMap()
  • groupingBy()

通过这些 API 操作,使得我们能将流中的元素收集起来

下面通过一些实际案例,加深对 Stream 的理解。

案例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class User {
private String name;
private int age;

User(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

现在有个需求筛选出年龄大于等于60的用户,然后将他们按照年龄从大到小排序,将他们的名字放在一个LinkedList中返回。我们可以使用 Stream 写出如下代码

1
2
3
4
5
6
7
public static LinkedList<String> collectNames(List<User> users) {
return users.stream()
.filter(user -> user.age >= 60)
.sorted(comparing(User::getAge).reversed())
.map(user -> user.name)
.collect(toCollection(LinkedList::new));
}

解析:要使用 Stream 首先就需要创建它users.stream(),根据要求筛选出年龄大于 60 的用户 filter(user -> user.age >= 60),然后使用 sorted 对筛选后的用户按照年龄排序。由于我们要返回的是一个字符串列表LinkedList<String>,而我们 Stream 中的元素都是对象类型,因此我们使用 map(user -> user.name),把User 映射成 String 类型,这样 Stream 中的元素都是用户的名字了。最后,使用 collect 操作将用户的名字收集成一个 Linked ,这样就解决了我们的需求。

通俗解释:我们作为老板,我们要创建一条流水线,然后安排工人在流水线旁边进行操作。安排员工老赵把用了60年以上的产品,筛选出来。筛选完后,安排老周按使用时长进行从大到小排序。排序之后,安排老王把可用的零件拆下来,最后用一个篮子收集这些可用的零件。

案例2

统计一个给定的字符串中,大写英文字母(A,B,C,…,Z)出现的次数。例如,给定字符串”AaBbCc1234ABC”,返回6,因为该字符串中出现了6次大写英文字母 ABCABC

1
2
3
4
5
public static int countUpperCaseLetters(String str) {
return (int) str.chars()
.filter(Character::isUpperCase)
.count();
}

解析:String 类型创建流 str.chars(),然后使用 filter 进行过滤操作,过滤出大写字母 filter(Character::isUpperCase),接着对过滤出的字母进行统计,完成需求。

更多关于 Stream 操作 demo点击这里

并发流

可以通过并发提高互相独立操作的性能。

互相独立操作指的就是,操作相互之间不影响。例:1个人割麦子需要10天,那10个人割相同面积的麦子就只需要1天。

非互相独立操作就像女人生孩子,1个女人十10个月生1个孩子,但是10个女人并不是一个月就能把孩子生出来。

在正确使用的前提下,可以获得近似线性的性能提升。要使用并发流我们可以使用 parallelStream()创建并发流。或者在原有流的基础上,使用 parallel() 方法将流转化为并发流。

使用一个简单的案例来使用并发流,统计 1 到 100万之间的质数个数 。原始的 Stream 写法如下

1
IntStream.range(1,100_0000).filter(是否为质数).count()

并发流写法

1
IntStream.range(1,100_0000).parallel().filter(isPrime).count();

因为统计个数属于互相独立操作,就像两个人数羊群有多少只绵羊一样,两人分别数自己区域的羊,然后再加起来汇总。

对于并发流来说,使⽤要⼩⼼,性能要测试,如果你不知道⾃⼰在做什 么,就忘了它吧。对于详细的操作可以参考《Effective Java 第三版》42-48节。


Stream
http://wszzf.top/2022/01/03/Stream/
作者
Greek
发布于
2022年1月3日
许可协议