操作 Stream
# 操作 Stream
简单说下 Stream 的操作
# map
Stream.map()
是 Stream
最常用的一个转换方法,它把一个 Stream
转换为另一个 Stream
。
所谓 map
操作,就是把一种操作运算,映射到一个序列的每一个元素上。例如,对 x
计算它的平方,可以使用函数 f(x) = x * x
。我们把这个函数映射到一个序列 1,2,3,4,5 上,就得到了另一个序列 1,4,9,16,25:
f(x) = x * x
│
│
┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
[ 1 2 3 4 5 6 7 8 9 ]
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
[ 1 4 9 16 25 36 49 64 81 ]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可见,map
操作,把一个 Stream
的每个元素一一对应到应用了目标函数的结果上。
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);
2
如果我们查看 Stream
的源码,会发现 map()
方法接收的对象是 Function
接口对象,它定义了一个 apply()
方法,负责把一个 T
类型转换成 R
类型:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
其中,Function
的定义是:
@FunctionalInterface
public interface Function<T, R> {
// 将T类型转换为R:
R apply(T t);
}
2
3
4
5
利用 map()
,不但能完成数学计算,对于字符串操作,以及任何 Java 对象都是非常有用的。例如:
Arrays.asList(" Peter ", "JXL", "heLLo")
.stream()
.map(String::trim)
.map(String::toLowerCase)
.forEach(System.out::println);
2
3
4
5
通过若干步 map
转换,可以写出逻辑简单、清晰的代码。
# filter
Stream.filter()
是 Stream
的另一个常用转换方法。
所谓 filter()
操作,就是对一个 Stream
的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的 Stream
。
例如,我们对 1,2,3,4,5 这个 Stream
调用 filter()
,传入的测试函数 f(x) = x % 2 != 0
用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列 1,3,5:
f(x) = x % 2 != 0
│
│
┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
[ 1 2 3 4 5 6 7 8 9 ]
│ X │ X │ X │ X │
│ │ │ │ │
↓ ↓ ↓ ↓ ↓
[ 1 3 5 7 9 ]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用 IntStream 写出上述逻辑,代码如下:
IntStream.of(1,2,3,4,5,6,7,8,9)
.filter(n -> n % 2 !=0)
.forEach(System.out::println);
2
3
从结果可知,经过 filter()
后生成的 Stream
元素可能变少。
filter()
方法接收的对象是 Predicate
接口对象,它定义了一个 test()
方法,负责判断元素是否符合条件:
@FunctionalInterface
public interface Predicate<T> {
// 判断元素t是否符合条件:
boolean test(T t);
}
2
3
4
5
filter()
除了常用于数值外,也可应用于任何 Java 对象。例如,从一组给定的 LocalDate
中过滤掉工作日,以便得到休息日:
import java.time.*;
import java.util.function.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream.generate(new LocalDateSupplier())
.limit(31)
.filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
.forEach(System.out::println);
}
}
class LocalDateSupplier implements Supplier<LocalDate> {
LocalDate start = LocalDate.of(2020, 1, 1);
int n = -1;
public LocalDate get() {
n++;
return start.plusDays(n);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# reduce
map()
和 filter()
都是 Stream
的转换方法,而 Stream.reduce()
则是 Stream
的一个聚合方法,它可以把一个 Stream
的所有元素按照聚合函数聚合成一个结果。
我们来看一个简单的聚合方法:
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.reduce(0, (acc, n) -> acc + n);
System.out.println(sum); // 45
}
}
2
3
4
5
6
7
8
9
reduce()
方法传入的对象是 BinaryOperator
接口,它定义了一个 apply()
方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:
@FunctionalInterface
public interface BinaryOperator<T> {
// Bi操作:两个输入,一个输出
T apply(T t, T u);
}
2
3
4
5
上述代码看上去不好理解,但我们用 for
循环改写一下,就容易理解了:
Stream<Integer> stream = ...
int sum = 0;
for (n : stream) {
sum = (sum, n) -> sum + n;
}
2
3
4
5
可见,reduce()
操作首先初始化结果为指定值(这里是 0),紧接着,reduce()
对每个元素依次调用 (acc, n) -> acc + n
,其中,acc
是上次计算的结果:
// 计算过程:
acc = 0 // 初始化为指定值
acc = acc + n = 0 + 1 = 1 // n = 1
acc = acc + n = 1 + 2 = 3 // n = 2
acc = acc + n = 3 + 3 = 6 // n = 3
acc = acc + n = 6 + 4 = 10 // n = 4
acc = acc + n = 10 + 5 = 15 // n = 5
acc = acc + n = 15 + 6 = 21 // n = 6
acc = acc + n = 21 + 7 = 28 // n = 7
acc = acc + n = 28 + 8 = 36 // n = 8
acc = acc + n = 36 + 9 = 45 // n = 9
2
3
4
5
6
7
8
9
10
11
因此,实际上这个 reduce()
操作是一个求和。
如果去掉初始值,我们会得到一个 Optional<Integer>
:
Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent()) {
System.out.println(opt.get());
}
2
3
4
这是因为 Stream
的元素有可能是 0 个,这样就没法调用 reduce()
的聚合函数了,因此返回 Optional
对象,需要进一步判断结果是否存在。
利用 reduce()
,我们可以把求和改成求积,代码也十分简单:
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
int s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(1, (acc, n) -> acc * n);
System.out.println(s); // 362880
}
}
2
3
4
5
6
7
8
注意:计算求积时,初始值必须设置为 1
。
除了可以对数值进行累积计算外,灵活运用 reduce()
也可以对 Java 对象进行操作。下面的代码演示了如何将配置文件的每一行配置通过 map()
和 reduce()
操作聚合成一个 Map<String, String>
:
import java.util.*;
public class Main {
public static void main(String[] args) {
// 按行读取配置文件:
List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
Map<String, String> map = props.stream()
// 把k=v转换为Map[k]=v:
.map(kv -> {
String[] ss = kv.split("\\=", 2);
return Map.of(ss[0], ss[1]);
})
// 把所有Map聚合到一个Map:
.reduce(new HashMap<String, String>(), (m, kv) -> {
m.putAll(kv);
return m;
});
// 打印结果:
map.forEach((k, v) -> {
System.out.println(k + " = " + v);
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
小结
reduce()
方法将一个 Stream
的每个元素依次作用于 BinaryOperator
,并将结果合并。
reduce()
是聚合方法,聚合方法会立刻对 Stream
进行计算。