《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 里的值生成一个 ListSetMap 或其他。

比如,生成 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

从一组值中生成一个值,上面用到的 countminmax 方法都属于 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