《Java 8 函数式编程》笔记2

流

从外部迭代到内部迭代

使用 for 循环统计来自美国的艺术家:

1
2
3
4
5
6
7
int count = 0;

for(Artist artist: allArtists) {
if(artist.isFrom("US")) {
count++;
}
}

for 循环本质是封装了迭代的语法糖,外部迭代:

1
2
3
4
5
6
7
8
9
int count = 0;

Iterator<Artist> iterator = allArtists.iterator();
while(iterator.hasNext()) {
Artist artist = (Artist) iterator.next();
if(artist.isFrom("US")) {
count++;
}
}

外部迭代本质上是一种串行化操作。

使用内部迭代改写:

1
2
3
4
5
// stream() 方法和上面的 iterator() 作用一样
// 但该方法返回的是内部迭代中的相应接口:Stream
int count = allArtists.stream()
.filter(artist -> artist.isFrom("US"))
.count();

注:Stream 是用函数式编程方式在集合类上进行复杂操作的工具

内部迭代的实现机制

只过滤,不计数:

1
2
allArtists.stream()
.filter(artist -> artist.isFrom("London"));

filter 只是刻画出了 Stream,并没有产生新的集合。

  • 这些不产生新集合方法叫:惰性求值方法;
  • 像 count 这样最终会从 Stream 产生值的方法叫:及早求值方法。

即使在 filter 过滤器中加上 println,也不会输出任何信息:

1
2
3
4
5
allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName);
return artist.isFrom("London");
});

但只要加入一个拥有终止操作的流,艺术家的名字就会被输出:

1
2
3
4
5
6
allArtists.stream()
.filter(artist -> {
System.out.print(artist.getName);
return artist.isFrom("London");
})
.count();

如何判断一个操作是惰性求值还是及早求值?

看它的返回值:

  • 返回值是 Stream:惰性求值;
  • 返回值是另一个值或 null:及早求值。

常用的流操作

collect()

由 Stream 里的值生成一个 List、Set、Map 或其他。

比如,生成 List:

1
2
3
4
5
6
// 使用 Stream 的 of 方法:
// 由一组初始值生成新的 Stream
List<String> collected = Stream.of("a", "b", "c")
.collect(Collectors.toList());

assert Arrays.asList("a", "b", "c").equals(collected);

map

将一个流中的值转换为一个新的流。

该 lambda 表达式的函数接口是 Function。

比如,将一组字符串都转为大小形式:

1
2
3
4
5
List<String> collected = Stream.of("a", "b", "abc")
.map(string -> string.toUpperCase())
.collect(Collectors.toList());

assert Arrays.asList("A", "B", "ABC").equals(collected);

filter

保留 Stream 中符合条件的元素,而过滤掉其他的。

该 lambda 表达式的函数接口是 Predicate。

比如,找出一组字符串中以数字开头的字符串:

1
2
3
4
5
List<String> startWithDigits = Stream.of("1a", "b", "abc")
.filter(string -> isDigit(string.charAt(0)))
.collect(Collectors.toList());

assert Arrays.asList("1a").equals(startWithDigits);

flatMap

将多个 Stream 连接成一个 Stream。

该 lambda 表达式的函数接口是 Function,返回值是 Stream。

比如,一个包含多个列表的 Stream 连接成只有一个列表的 Stream:

1
2
3
4
5
List<Integer> together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(Collectors.toList());

assert Arrays.asList(1, 2, 3, 4).equals(together);

max 和 min

找出 Stream 中的最大最小值。

比如,找出播放长度最短的曲目:

1
2
3
4
5
6
7
8
9
10
List<Track> tracks = Arrays.asList(
new Track("BaKai", 524),
new Track("Violets", 378),
new Track("Time Was", 451));

Track shortestTrack = tracks.stream()
.min(Comparator.comparing(track -> track.getLength()))
.get();

assert tracks.get(1).equals(shortestTrack);

reduce

从一组值中生成一个值,上面用到的 count、min 和 max 方法都属于 reduce 操作。因为常用而被纳入标准库。

比如,求和:

1
2
3
4
5
// 0 是初始值,acc 是累加器
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element);

assert 6 == count;

阶乘:

1
2
3
BigInteger k = Stream.iterate(BigInteger.ONE, x -> x.add(BigInteger.ONE))
.limit(n)
.reduce(BigInteger.ONE, (m, current) -> m.multiply(current));

整合操作

举例说明如何把问题分解成简单的 Stream 操作:

如何找出某张专辑上乐队所有成员的国籍?

将问题分解:

  1. 找出专辑上的所有表演者
  2. 分辨出哪些表演者是乐队
  3. 找出乐队每个中每个成员的国籍
  4. 将找出的国籍放在一个集合里

找出对应的 Stream API:

  1. 专辑 Album 类有 getMusicians 方法,该方法返回一个 Stream 对象,包含整张专辑中所有的表演者
  2. 使用 filter 方法对表演者进行过滤,只保留乐队
  3. 使用 flatMap 方法将乐队成员加入流中
  4. 使用 map 方法将成员映射为其所属国家
  5. 使用 collect 方法将找出的国籍放到集合里
1
2
3
4
5
Set<String> origins = album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.flatMap(artist -> artist.getMembers())
.map(member -> member.getNationality())
.collect(Collectors.toSet());
Author

Zoctan

Posted on

2018-03-05

Updated on

2023-03-14

Licensed under