文章目录
- 前言
- 一、方法引用
- 二、元素顺序
- 三、使用收集器
- 1.转换成其他集合
- 2.转换成值
- 3.数据分块
- 4.数据分组
- 5.字符串
- 6.组合收集器
- 总结
前言
前面介绍了集合类的部分变化,事实上,Java 8 对集合类的改进不止这些。现在是时候介绍一些高级主题了,包括新引入的 Collector 类。同时介绍方法引用,它可以帮助大家在 Lambda 表达式中轻松使用已有代码。编写大量使用集合类的代码时,使用方法引用能让程序员获得丰厚的回报。
一、方法引用
Lambda 表达式有一个常见的用法:Lambda 表达式经常调用参数。比如想得到艺术家的姓名,Lambda 的表达式如下:
artist -> artist.getName()
这种用法如此普遍,因此 Java 8 为其提供了一个简写语法,叫作方法引用,帮助程序员重用已有方法。用方法引用重写上面的 Lambda 表达式,代码如下:
Artist::getName
标准语法为 Classname::methodName。需要注意的是,虽然这是一个方法,但不需要在后面加括号,因为这里并不调用该方法。我们只是提供了和 Lambda 表达式等价的一种结构,在需要时才会调用。凡是使用 Lambda 表达式的地方,就可以使用方法引用。
构造函数也有同样的缩写形式,如果你想使用 Lambda 表达式创建一个 Artist 对象,可能会写出如下代码:
(name, nationality) -> new Artist(name, nationality)
使用方法引用,上述代码可写为:
Artist::new
这段代码不仅比原来的代码短,而且更易阅读。Artist::new 立刻告诉程序员这是在创建一个 Artist 对象,程序员无需看完整行代码就能弄明白代码的意图。另一个要注意的地方是方法引用自动支持多个参数,前提是选对了正确的函数接口。
二、元素顺序
另外一个尚未提及的关于集合类的内容是流中的元素以何种顺序排列。我们可能知道,一些集合类型中的元素是按顺序排列的,比如 List;而另一些则是无序的,比如 HashSet。增加了流操作后,顺序问题变得更加复杂。
直观上看,流是有序的,因为流中的元素都是按顺序处理的。这种顺序称为出现顺序
。出现顺序的定义依赖于数据源和对流的操作。
在一个有序集合中创建一个流时,流中的元素就按出现顺序排列,如下:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
List<Integer> list = numbers.stream().collect(Collectors.toList());
System.out.println(numbers);
System.out.println(list);
如果集合本身就是无序的(及插入顺序和输入顺序不一致),由此生成的流也是无序的,流中的元素就会按照集合的出现顺序
进行处理。
Set<Integer> integerSet = new HashSet<>();
integerSet.add(1);
integerSet.add(22);
integerSet.add(500);
System.out.println(integerSet);
integerSet.stream().forEach(integer -> System.out.print(integer + ","));
流的目的不仅是在集合类之间做转换,而且同时提供了一组处理数据的通用操作。有些集合本身是无序的,但有些操作有时会产生顺序:
Set<Integer> integerSet = new HashSet<>();
integerSet.add(6);
integerSet.add(3);
integerSet.add(5);
List<Integer> sameOrder = integerSet.stream().sorted().collect(Collectors.toList());
System.out.println(sameOrder);
三、使用收集器
前面使用过 collect(Collectors.toList()),在流中生成列表。显然,List 是能想到的从流中生成的最自然的数据结构,但是有时人们还希望从流生成其他值,比如 Map 或 Set,或者你希望定制一个类将你想要的东西抽象出来。
前面已经讲过,仅凭流上方法的签名,就能判断出这是否是一个及早求值的操作。reduce 操作就是一个很好的例子,但有时人们希望能做得更多。
这就是收集器
,一种通用的、从流生成复杂值的结构。只要将它传给 collect 方法,所有的流就都可以使用它了。
1.转换成其他集合
有一些收集器可以生成其他集合。比如前面已经见过的 Collectors.toList(),生成了 java.util.List 类的实例。到目前为止,了解了很多流上的链式操作,但总有一些时候,需要最终生成一个集合——比如:
- 已有代码是为集合编写的,因此需要将流转换成集合传入;
- 在集合上进行一系列链式操作后,最终希望生成一个值;
- 写单元测试时,需要对某个具体的集合做断言。
通常情况下,创建集合时需要调用适当的构造函数指明集合的具体类型:
List<Artist> artists = new ArrayList<>();
但是调用 toList 或者 toSet 方法时,不需要指定具体的类型。Stream 类库在背后自动为你挑选出了合适的类型。
可能还会有这样的情况,你希望使用一个特定的集合收集值,而且你可以稍后指定该集合的类型。比如,你可能希望使用 TreeSet,而不是由框架在背后自动为你指定一种类型的Set。此时就可以使用toCollection,它接受一个函数作为参数,来创建集合。
stream.collect(Collectors.toCollection(TreeSet::new));
2.转换成值
还可以利用收集器让流生成一个值。maxBy 和 minBy 允许用户按某种特定的顺序生成一个值。
public Optional<Artist> biggestGroup(Stream<Artist> artists) {
Function<Artist,Long> getCount = artist -> artist.getArtists().stream().count();
return artists.max(Comparator.comparing(getCount));
}
minBy 就如它的方法名,是用来找出最小值的。
3.数据分块
另外一个常用的流操作是将其分解成两个集合。。假设有一个艺术家组成的流,你可能希望将其分成两个部分,一部分是独唱歌手,另一部分是由多人组成的乐队。可以使用两次过滤操作,分别过滤出上述两种艺术家。
但是这样操作起来有问题。首先,为了执行两次过滤操作,需要有两个流。其次,如果过滤操作复杂,每个流上都要执行这样的操作,代码也会变得冗余。
有这样一个收集器 partitioningBy,它接受一个流,并将其分成两部分。它使用 Predicate 对象判断一个元素应该属于哪个部分,并根据布尔值返回一个 Map 到列表。因此,对于 true List 中的元素,Predicate 返回 true;对其他 List 中的元素,Predicate 返回 false。
使用它,我们就可以将乐队(有多个成员)和独唱歌手分开了。
public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) {
return artists.collect(Collectors.partitioningBy(Artist::isSolo));
}
4.数据分组
数据分组是一种更自然的分割数据操作,与将数据分成 ture 和 false 两部分不同,可以使用任意值对数据分组。比如现在有一个由专辑组成的流,可以按专辑当中的主唱对专辑分组。
public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) {
return albums.collect(Collectors.groupingBy(album -> album.getMainMusician()));
}
和其他一样,调用流的 collect 方法,传入一个收集器。groupingBy 收集器接受一个分类函数,用来对数据分组,就像 partitioningBy 一样,接受一个 Predicate 对象将数据分成 ture 和 false 两部分。我们使用的分类器是一个 Function 对象,和 map 操作用到的一样。
我们知道SQL 中的 group by 操作,groupingBy方法是和这类似的一个概念,只不过在 Stream 类库中实现了而已
。
5.字符串
很多时候,收集流中的数据都是为了在最后生成一个字符串。假设我们想将参与制作一张专辑的所有艺术家的名字输出为一个格式化好的列表。
Java 8 还未发布前,实现该功能的代码可能通过不断迭代列表,使用一个 StringBuilder 对象来记录结果。每一步都取出一个艺术家的名字,追加到 StringBuilder对象。使用 Java 8 提供的流和收集器就能写出更清晰的代码:
String result = artists.stream().map(Artist::getName).collect(Collectors.joining(", ", "[", "]"));
这里使用 map 操作提取出艺术家的姓名,然后使用 Collectors.joining 收集流中的值,该方法可以方便地从一个流得到一个字符串,允许用户提供分隔符(用以分隔元素)、前缀和后缀。
6.组合收集器
前面看到的各种收集器已经很强大了,但如果将它们组合起来,会变得更强大。之前我们使用主唱将专辑分组,现在来考虑如何计算一个艺术家的专辑数量。
public Map<Artist, Long> numberOfAlbums(Stream<Album> albums) {
return albums.collect(Collectors.groupingBy(album -> album.getMainMusician(), Collectors.counting()));
}
groupingBy 先将元素分成块,每块都与分类函数 getMainMusician 提供的键值相关联,然后使用下游的另一个收集器收集每块中的元素,最后将结果映射为一个 Map。
总结
方法引用是一种引用方法的轻量级语法,形如:ClassName::methodName,收集器可用来计算流的最终值,是 reduce 方法的模拟。