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

高级集合类和收集器

方法引用

标准语法:Classname::methodName

比如想得到艺术家的名字:

1
2
3
4
5
lambda:artist -> artist.getName()

方法引用:Artist::getName

Arrays.stream(artist).map(Artist::getName).forEach(System.out::println)

构造方法同样可以缩写:

1
2
3
lambda:(name, nationality) -> new Artist(name, nationality)

方法引用:Artist::new

元素顺序

本身是有序集合,比如 List,创建流时,流中的元素就有顺序:

1
2
3
4
5
6
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);

List<Integer> sameOrder = numbers.stream()
.collect(Collectors.toList());

assert sameOrder.equals(numbers);

本身是无序集合,比如 HashSet,由此生成的流也是无序的:

1
2
3
4
5
6
Set<Integer> numbers = new HashSet<>(Arrays.asList(4, 3, 2, 1));

List<Integer> sameOrder = numbers.stream()
.collect(Collectors.toList());
// 断言有时会失败
assert Arrays.asList(4, 3, 2, 1).equals(sameOrder);

可以使用 sorted(),让流里的元素有序:

1
2
3
4
5
6
7
Set<Integer> numbers = new HashSet<>(Arrays.asList(4, 3, 2, 1));

List<Integer> sameOrder = numbers.stream()
.sorted()
.collect(Collectors.toList());

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

或者使用 unordered(),变无序:

1
2
3
4
5
6
7
Set<Integer> numbers = new HashSet<>(Arrays.asList(4, 3, 2, 1));

List<Integer> sameOrder = numbers.stream()
.unordered()
.collect(Collectors.toList());

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

使用收集器

collect(Collectors.toList()),在流中生成列表。
类似的还有 MapSet 等。

转换为其他集合

比如转换为 TreeSet,而不是框架背后为你指定的一种类型的 Set

1
2
3
List<Integer> numbers = Arrays.asList(4, 3, 2, 1);
Set<Integer> treeSet = numbers.stream()
.collect(Collectors.toCollection(TreeSet::new));

转换为值

maxByminBy

找出成员最多的乐队:

1
2
3
4
public Optional<Artist> biggestGroup(Stream<Artist> artists) {
Function<Artist, Long> getCount = artist -> artist.getMembers().count();
return artists.collect(maxBy(comparing(getCount)));
}

找出一组专辑上单曲的平均数:

1
2
3
public double averageNumberOfTracks(List<Album> albums) {
return albums.stream().collect(averagingInt(album -> album.getTrackList().size()));
}

数据分块

假设有一个艺术家组成的流,一部分是独唱歌手,另一部分是乐队。
如果你希望将其分成两部分,可以使用收集器 partitioningBy,它接受一个流, 并将其分成两部分:

1
2
3
public Map<Boolean, List<Artist>> soloAndBands(Stream<Artist> artists) {
return artists.collect(partitioningBy(Artist::isSolo));
}

数据分组

与将数据分成 truefalse 两块不同,数据分组是一种更自然的分割数据操作,可以使用任意值对数据分组。

比如,现在有一个专辑组成的流,可以按专辑当中的乐队主唱对专辑分组:

1
2
3
public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) {
return albums.collect(groupingBy(Album::getMainMusician));
}

字符串

比如要得到 “[{A, B, C}]” 这样的字符串:

1
2
3
4
5
public String getString() {
List<String> strings = Arrays.asList("A", "B", "C");
return strings.stream()
.collect(Collectors.joining(", ", "[{", "}]"));
}

Collectors.joining(分隔符, 前缀, 后缀)

组合收集器

如何计算一个艺术家的发行的专辑数量?

最简单的就是使用前面的方法:对专辑先按艺术家分组,然后计数:

1
2
3
4
5
6
Map<Artist, List<Album>> albumsByArtist = albums.collect(groupingBy(Album::getMainMusician));

Map<Artist, Integer> numberOfAlbums = new HashMap<>();
for (Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {
numberOfAlbums.put(entry.getKey(), entry.getValue().size());
}

这段代码固然简单,但有点杂乱,命令式的代码,也无法自动适应并行化的操作。

使用 counting 重写:

1
2
3
4
Map<Artist, Long> numberOfAlbums = albums.collect(
groupingBy(Album::getMainMusician,
counting())
);

groupingBy 先将元素分块,每块都与 getMainMusician 提供的键相关联,然后使用下游的另一个收集器收集每块中的元素,最后将结果映射为 Map

另一个例子:如何获得每个艺术家的每张专辑名,而不是每张专辑?

1
2
3
4
5
6
7
8
9
Map<Artist, List<Album>> albumsByArtist = albums.collect(groupingBy(Album::getMainMusician));

Map<Artist, List<String>> nameOfAlbums = new HashMap<>();
for (Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {
nameOfAlbums.put(entry.getKey(), entry.getValue()
.stream()
.map(Album::getName)
.collect(toList()));
}

groupingBy 将专辑按主唱分组,输出了 Map<Artist, List<Album>>,它将每个艺术家和他的专辑列表关联起来。

但我们需要的是 Map<Artist, List<String>>,将每个艺术家和他的专辑名列表关联起来。

mapping 可以像 map 一样将 groupingBy 的值做映射,生成我们想要的结果:

1
2
3
4
5
albums.collect(
groupingBy(Album::getMainMusician,
mapping(Album::getName,
toList()))
);

Map 类的变化

Map 实现缓存,传统方法:先试着取值,如果值为空,创建一个新值并返回。

1
2
3
4
5
6
7
8
public Artist getArtist(String name) {
Artist artist = artistCache.get(name);
if (artist == null) {
artist = readArtistFromDB(name);
artistCache.put(name, artist);
}
return artist;
}

computeIfAbsent 方法会在值不存在时,使用 lambda 表达式计算新值:

1
2
3
public Artist getArtistUsingComputeIfAbsent(String name) {
return artistCache.computeIfAbsent(name, this::readArtistFromDB);
}

你可能试过在 Map 上迭代,比如:

1
2
3
4
5
6
7
8
9
Map<Artist, List<Album>> albumsByArtist = albums.collect(groupingBy(Album::getMainMusician));

Map<Artist, Integer> numberOfAlbums = new HashMap<>();

for (Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {
Artist artist = entry.getKey();
List<Album> albums = entry.getValue();
numberOfAlbums.put(artist, albums.size());
}

虽然工作正常,但是看起来挺丑的。

使用 forEach 内部迭代 Map 里的值:

1
2
3
4
5
6
7
Map<Artist, List<Album>> albumsByArtist = albums.collect(groupingBy(Album::getMainMusician));

Map<Artist, Integer> numberOfAlbums = new HashMap<>();

albumsByArtist.forEach(
(artist, albumList) -> numberOfAlbums.put(artist, albumList.size())
);
Author

Zoctan

Posted on

2018-03-05

Updated on

2023-03-14

Licensed under