1,免责声明,本文大部分内容摘自《Java8函数式编程》。在这本书的基础上,根据自己的理解和网上一些博文,精简或者修改。本次分享的内容,只用于技术分享,不作为任何商业用途。当然这本书是非常值得一读,强烈建议买一本!
2,本次分享的样例代码均上传到github上,请点击这里。
注意:本章所有的例子大多围绕 1.3 节介绍的案例展开(音乐)。
第5章 高级集合类和收集器
第3章只介绍了集合类的部分变化,事实上,Java 8对集合类的改进不止这些。现在是时候介绍一些高级主题了,包括新引入的 Collector
类。同时我还会为大家介绍方法引用, 它可以帮助大家在 Lambda 表达式中轻松使用已有代码。编写大量使用集合类的代码时, 使用方法引用能让程序员获得丰厚的回报。本章还会涉及集合类的一些更高级的主题,比如流中元素的顺序,以及一些有用的 API。
主要内容如下:
- 5.1 方法引用
- 5.2 元素顺序
- 5.3 使用收集器(重点)
- 5.4 一些细节
- 5.5 要点回顾
5.1 方法引用
读者可能已经发现,Lambda 表达式有一个常见的用法:Lambda 表达式经常调用参数。比如想得到艺术家的姓名,Lambda 的表达式如下:
artist -> artist.getName()
这种用法如此普遍,因此 Java8 为其提供了一个简写语法,叫作 方法引用,帮助程序员重用已有方法。用方法引用重写上面的 Lambda 表达式,代码如下:
Artist::getName
标准语法为 Classname::methodName
。需要注意的是,虽然这是一个方法,但不需要在后面加括号,因为这里并不会立即调用该方法。我们只是提供了和 Lambda 表达式等价的一种结构,在需要时才会调用。凡是使用 Lambda 表达式的地方,就可以使用方法引用。
构造函数也有同样的缩写形式,如果你想使用 Lambda 表达式创建一个 Artist 对象,可能会写出如下代码:
(name, nationality) -> new Artist(name, nationality)
使用方法引用,上述代码可写为:
Artist::new
这段代码不仅比原来的代码短,而且更易阅读。Artist::new
立刻告诉程序员这是在创建一个 Artist 对象,程序员无需看完整行代码就能弄明白代码的意图。另一个要注意的地方是:方法引用自动支持多个参数,前提是选对了正确的函数接口。
还可以用这种方式创建数组,下面的代码创建了一个字符串型的数组:
String[]::new
5.2 元素顺序
关于集合类的内容一个尚未提及的是:流中的元素以何种顺序排列。读者可能知道,一些集合类型中的元素是按顺序排列的,比如 List
;而另一些则是无序的,比如 HashSet
。增加了流操作后,顺序问题变得更加复杂。
直观上看,流是有序的,因为流中的元素都是按顺序处理的。这种顺序称为 出现顺序。出现顺序的定义依赖于数据源和对流的操作。
在一个有序集合中创建一个流时,流中的元素就按出现顺序排列,因此,例 5-1 中的代码总是可以通过。
// 例 5-1 顺序测试永远通过
List<Integer> numbers = asList(1, 2, 3, 4);
List<Integer> sameOrder = numbers.stream()
.collect(toList());
assertEquals(numbers, sameOrder);
如果集合本身就是无序的,由此生成的流也是无序的。HashSet
就是一种无序的集合,因此不能保证例 5-2 所示的程序每次都通过。
// 例 5-2 顺序测试不能保证每次通过
Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
List<Integer> sameOrder = numbers.stream()
.collect(toList());
// 该断言有时会失败
assertEquals(asList(4, 3, 2, 1), sameOrder);
流的目的不仅是在集合类之间做转换,而且同时提供了一组处理数据的通用操作。有些集合本身是无序的,但这些操作有时会产生顺序, 试看例 5-3 中的代码。
// 例 5-3 生成出现顺序
Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
List<Integer> sameOrder = numbers.stream()
.sorted()
.collect(toList());
assertEquals(asList(1, 2, 3, 4), sameOrder);
一些中间操作会产生顺序,比如对值做映射时,映射后的值是有序的,这种顺序就会保留下来。如果进来的流是无序的,出去的流也是无序的。看一下例 5-4 所示代码,我们只能断言 HashSet
中含有某元素,但对其顺序不能作出任何假设,因为 HashSet 是无序的,使用了映射操作后,得到的集合仍然是无序的。
// 例 5-4 本例中关于顺序的假设永远是正确的
List<Integer> numbers = asList(1, 2, 3, 4);
List<Integer> stillOrdered = numbers.stream()
.map(x -> x + 1)
.collect(toList());
// 顺序得到了保留
assertEquals(asList(2, 3, 4, 5), stillOrdered);
Set<Integer> unordered = new HashSet<>(numbers);
List<Integer> stillUnordered = unordered.stream()
.map(x -> x + 1)
.collect(toList());
// 顺序得不到保证
assertThat(stillUnordered, hasItem(2));
assertThat(stillUnordered, hasItem(3));
assertThat(stillUnordered, hasItem(4));
assertThat(stillUnordered, hasItem(5));
一些操作在有序的流上开销更大,调用 unordered
方法消除这种顺序就能解决该问题。但大多数操作都是在有序流上效率更高,比如 filter
、map
和 reduce
等。
这会带来一些意想不到的结果,比如使用并行流时,forEach
方法不能保证元素是按顺序处理的 (第 6 章会详细讨论这些内容)。如果需要保证按顺序处理,应该使用 forEachOrdered
方法,它才是你保证顺序的朋友。
5.3 使用收集器
前面我们使用过 collect(toList())
,在流中生成列表。显然,List
是最常用的数据结构,但有时候我们希望从流生成其它数据结构,比如 Map
或 Set
,或者你希望定制一个类将你想要的东西抽象出来。
前面已经讲过,仅凭流上方法的返回值,就能判断出这是否是一个及早求值的操作。reduce
操作就是一个很好的例子,但有时我们希望能做得更多。
这就是收集器,一种通用的、从流生成复杂值的结构。只要将它传给 collect
方法,所有的流就都可以使用它了。
标准类库已经提供了一些有用的收集器,让我们先来看看。本章示例代码中的收集器都是 从java.util.stream.Collectors
类中静态导入的。
5.3.1 转换成其他集合
有一些收集器可以生成其它集合。比如前面已经见过的 toList
,生成了 java.util.List
类的实例。还有 toSet
和 toCollection
,分别生成 Set
和 Collection
类的实例。到目前为止, 我已经讲了很多流上的链式操作,但总有一些时候&#x