17
17
虽然函数定义越来越长,但语义不曾改变,多的参数只是为了指明初始值(参数* identity* ),或者是指定并行执行时多个部分结果的合并方式(参数* combiner* )。` reduce() ` 最常用的场景就是从一堆值中生成一个值。用这么复杂的函数去求一个最大或最小值,你是不是觉得设计者有病。其实不然,因为“大”和“小”或者“求和"有时会有不同的语义。
18
18
19
19
需求:* 从一组单词中找出最长的单词* 。这里“大”的含义就是“长”。
20
-
21
20
``` Java
22
21
// 找出最长的单词
23
22
Stream<String > stream = Stream . of(" I" , " love" , " you" , " too" );
24
23
Optional<String > longest = stream. reduce((s1, s2) - > s1. length()>= s2. length() ? s1 : s2);
25
24
// Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
26
25
System . out. println(longest. get());
27
26
```
28
-
29
27
上述代码会选出最长的单词* love* ,其中* Optional* 是(一个)值的容器,使用它可以避免* null* 值的麻烦。当然可以使用` Stream.max(Comparator<? super T> comparator) ` 方法来达到同等效果,但` reduce() ` 自有其存在的理由。
30
28
31
29
<img src =" ./Figures/Stream.reduce_parameter.png " width =" 400px " align =" right " alt =" Stream.reduce_parameter " />
32
30
33
31
需求:* 求出一组单词的长度之和* 。这是个“求和”操作,操作对象输入类型是* String* ,而结果类型是* Integer* 。
34
-
35
32
``` Java
36
33
// 求单词长度之和
37
34
Stream<String > stream = Stream . of(" I" , " love" , " you" , " too" );
@@ -48,15 +45,13 @@ System.out.println(lengthSum);
48
45
## >>> 终极武器collect() <<<
49
46
50
47
不夸张的讲,如果你发现某个功能在* Stream* 接口中没找到,十有八九可以通过` collect() ` 方法实现。` collect() ` 是* Stream* 接口方法中最灵活的一个,学会它才算真正入门Java函数式编程。先看几个热身的小例子:
51
-
52
48
``` Java
53
49
// 将Stream转换成容器或Map
54
50
Stream<String > stream = Stream . of(" I" , " love" , " you" , " too" );
55
51
List<String > list = stream. collect(Collectors . toList()); // (1)
56
52
// Set<String> set = stream.collect(Collectors.toSet()); // (2)
57
53
// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)
58
54
```
59
-
60
55
上述代码分别列举了如何将* Stream* 转换成* List* 、* Set* 和* Map* 。虽然代码语义很明确,可是我们仍然会有几个疑问:
61
56
62
57
1 . ` Function.identity() ` 是干什么的?
@@ -89,7 +84,7 @@ List<String> list = stream.collect(Collectors.toList()); // (1)
89
84
90
85
相信前面繁琐的内容已彻底打消了你学习Java函数式编程的热情,不过很遗憾,下面的内容更繁琐。
91
86
92
- <img src="./Figures/Stream.collect_parameter.png" width="400px ", align="right" alt="Stream.collect_parameter" />
87
+ <img src="./Figures/Stream.collect_parameter.png" width="500px ", align="right" alt="Stream.collect_parameter" />
93
88
94
89
收集器(* Collector* )是为` Stream.collect() ` 方法量身打造的工具接口(类)。考虑一下将一个* Stream* 转换成一个容器(或者* Map* )需要做哪些工作?我们至少需要两样东西:
95
90
@@ -99,36 +94,30 @@ List<String> list = stream.collect(Collectors.toList()); // (1)
99
94
如果并行的进行规约,还需要告诉* collect()* 3. 多个部分结果如何合并成一个。
100
95
101
96
结合以上分析,* collect()* 方法定义为` <R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner) ` ,三个参数依次对应上述三条分析。不过每次调用* collect()* 都要传入这三个参数太麻烦,收集器* Collector* 就是对这三个参数的简单封装,所以* collect()* 的另一定义为` <R,A> R collect(Collector<? super T,A,R> collector) ` 。* Collectors* 工具类可通过静态方法生成各种常用的* Collector* 。举例来说,如果要将* Stream* 规约成* List* 可以通过如下两种方式实现:
102
-
103
97
``` Java
104
98
// 将Stream规约成List
105
99
Stream<String > stream = Stream . of(" I" , " love" , " you" , " too" );
106
100
List<String > list = stream. collect(ArrayList :: new , ArrayList::add, ArrayList::addAll);// 方式1
107
101
// List<String> list = stream.collect(Collectors.toList());// 方式2
108
102
System . out. println(list);
109
103
```
110
-
111
104
通常情况下我们不需要手动指定* collect()* 的三个参数,而是调用`collect(Collector<? super T ,A ,R > collector)`方法,并且参数中的* Collector * 对象大都是直接通过* Collectors * 工具类获得。实际上传入的** 收集器的行为决定了`collect()`的行为** 。
112
105
113
106
## 使用collect()生成Collection
114
107
115
108
前面已经提到通过`collect()`方法将* Stream * 转换成容器的方法,这里再汇总一下。将* Stream * 转换成* List * 或* Set * 是比较常见的操作,所以* Collectors * 工具已经为我们提供了对应的收集器,通过如下代码即可完成:
116
-
117
109
```Java
118
110
// 将Stream转换成List或Set
119
111
Stream<String > stream = Stream . of(" I" , " love" , " you" , " too" );
120
112
List<String > list = stream. collect(Collectors . toList()); // (1)
121
113
Set<String > set = stream. collect(Collectors . toSet()); // (2)
122
114
```
123
-
124
115
上述代码能够满足大部分需求,但由于返回结果是接口类型,我们并不知道类库实际选择的容器类型是什么,有时候我们可能会想要人为指定容器的实际类型,这个需求可通过`Collectors . toCollection(Supplier<C > collectionFactory)`方法完成。
125
-
126
116
```Java
127
117
// 使用toCollection()指定规约容器的类型
128
118
ArrayList<String > arrayList = stream. collect(Collectors . toCollection(ArrayList :: new ));// (3)
129
119
HashSet<String > hashSet = stream. collect(Collectors . toCollection(HashSet :: new ));// (4)
130
120
```
131
-
132
121
上述代码(3 )处指定规约结果是* ArrayList * ,而(4 )处指定规约结果为* HashSet * 。一切如你所愿。
133
122
134
123
## 使用collect()生成Map
@@ -140,63 +129,51 @@ HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));
140
129
3. 使用`Collectors . groupingBy()`生成的收集器,对元素做* group* 操作时用到。
141
130
142
131
情况1 :使用`toMap()`生成的收集器,这种情况是最直接的,前面例子中已提到,这是和`Collectors . toCollection()`并列的方法。如下代码展示将学生列表转换成由< 学生,GPA > 组成的* Map * 。非常直观,无需多言。
143
-
144
132
```Java
145
133
// 使用toMap()统计学生GPA
146
134
Map<Student , Double > studentToGPA =
147
135
students. stream(). collect(Collectors . toMap(Functions . identity(),// 如何生成key
148
136
student - > computeGPA(student)));// 如何生成value
149
137
```
150
-
151
138
情况2 :使用`partitioningBy()`生成的收集器,这种情况适用于将`Stream `中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分,比如男女性别、成绩及格与否等。下列代码展示将学生分成成绩及格或不及格的两部分。
152
-
153
139
```Java
154
140
// Partition students into passing and failing
155
141
Map<Boolean , List<Student > > passingFailing = students. stream()
156
142
.collect(Collectors . partitioningBy(s - > s. getGrade() >= PASS_THRESHOLD ));
157
143
```
158
-
159
144
情况3 :使用`groupingBy()`生成的收集器,这是比较灵活的一种情况。跟SQL 中的* group by* 语句类似,这里的* groupingBy()* 也是按照某个属性对数据进行分组,属性相同的元素会被对应到* Map * 的同一个* key* 上。下列代码展示将员工按照部门进行分组:
160
-
161
145
```Java
162
146
// Group employees by department
163
147
Map<Department , List<Employee > > byDept = employees. stream()
164
148
.collect(Collectors . groupingBy(Employee :: getDepartment));
165
149
```
166
-
167
150
以上只是分组的最基本用法,有些时候仅仅分组是不够的。在SQL 中使用* group by* 是为了协助其他查询,比如* 1. 先将员工按照部门分组,2. 然后统计每个部门员工的人数* 。Java 类库设计者也考虑到了这种情况,增强版的`groupingBy()`能够满足这种需求。增强版的`groupingBy()`允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做** 上游收集器** ,之后执行其他运算的收集器叫做** 下游收集器** (* downstream Collector * )。
168
-
169
151
```Java
170
152
// 使用下游收集器统计每个部门的人数
171
153
Map<Department , Integer > totalByDept = employees. stream()
172
154
.collect(Collectors . groupingBy(Employee :: getDepartment,
173
155
Collectors . counting()));// 下游收集器
174
156
```
175
-
176
157
上面代码的逻辑是不是越看越像SQL ?高度非结构化。还有更狠的,下游收集器还可以包含更下游的收集器,这绝不是为了炫技而增加的把戏,而是实际场景需要。考虑将员工按照部门分组的场景,如果* 我们想得到每个员工的名字(字符串),而不是一个个* Employee * 对象* ,可通过如下方式做到:
177
-
178
158
```Java
179
159
// 按照部门对员工分布组,并只保留员工的名字
180
160
Map<Department , List<String > > byDept = employees. stream()
181
161
.collect(Collectors . groupingBy(Employee :: getDepartment,
182
162
Collectors . mapping(Employee :: getName,// 下游收集器
183
163
Collectors . toList())));// 更下游的收集器
184
164
```
185
-
186
165
如果看到这里你还没有对Java 函数式编程失去信心,恭喜你,你已经顺利成为Java 函数式编程大师了。
187
166
188
167
## 使用collect()做字符串join
189
168
190
169
这个肯定是大家喜闻乐见的功能,字符串拼接时使用`Collectors . joining()`生成的收集器,从此告别* for * 循环。`Collectors . joining()`方法有三种重写形式,分别对应三种不同的拼接方式。无需多言,代码过目难忘。
191
-
192
170
```Java
193
171
// 使用Collectors.joining()拼接字符串
194
172
Stream<String > stream = Stream . of(" I" , " love" , " you" );
195
173
// String joined = stream.collect(Collectors.joining());// "Iloveyou"
196
174
// String joined = stream.collect(Collectors.joining(","));// "I,love,you"
197
175
String joined = stream. collect(Collectors . joining(" ," , " {" , " }" ));// "{I,love,you}"
198
176
```
199
-
200
177
## collect()还可以做更多
201
178
202
179
除了可以使用* Collectors * 工具类已经封装好的收集器,我们还可以自定义收集器,或者直接调用`collect(Supplier<R > supplier, BiConsumer<R ,? super T > accumulator, BiConsumer<R ,R > combiner)`方法,** 收集任何形式你想要的信息** 。不过* Collectors * 工具类应该能满足我们的绝大部分需求,手动实现之间请先看看文档。
0 commit comments