Java SE 8 Lambda 标准库概览
8,Collectors
在目前的例子中,我们已经使用了collect()方法来收集流中的元素并放入List或Set中了。collec()方法的参数是一个收集器,它包含了收集或汇总多个元素到一个数据结构中的概念。Collectors 类提供了诸多常用收集器的工厂方法;toList()和toSet()是最常用的两种,但是还有很多可以用来执行复杂转换的收集器。
收集器的输入和输出均为参数化的。toList() 的 收集器接受一个T类型的输入并产生一个List
Map<Integer, Album> albumsByCatalogNumber =
albums.stream()
.collect(Collectors.toMap(a -> a.getCatalogNumber(), a -> a));
与toMap相关的是groupingBy。假如我们想要将最喜欢的歌曲以歌手分组制成表格。我们希望有一个接受歌曲(Track)为输入,Map<Artist, List
Map<Artist, List<Track>> favsByArtist =
tracks.stream()
.filter(t -> t.rating >= 4)
.collect(Collectors.groupingBy(t -> t.artist));
收集器可以被组合重用来产生更加复杂的收集器。groupingBy的简单形式根据分类函数将元素组织到桶位中(这里是歌手),然后将所有映射到同一个桶位的元素放入List中。还有一个更加通用的版本允许我们使用另一个收集器来组织同一个桶位中的元素;这个版本接受一个分类函数,以及一个下游收集器作为参数,并且根据分类函数映射到同一个桶位的所有元素被收集到下游收集器中。(一个参数版本的groupingBy隐式的使用toLit()作为下游收集器)举个例子,如果我们想要收集不同歌手的歌曲到一个Set中而不是List,我们可以使用toSet()收集器:
Map<Artist, Set<Track>> favsByArtist =
tracks.stream()
.filter(t -> t.rating >= 4)
.collect(Collectors.groupingBy(t -> t.artist,
Collectors.toSet()));
如果我们想要将歌曲以歌手和评价等级分类,来创建一个多层映射,我们可以这样:
Map<Artist, Map<Integer, List<Track>>> byArtistAndRating =
tracks.stream()
.collect(groupingBy(t -> t.artist,
groupingBy(t -> t.rating)));
最后一个例子,假如我们想要创建一个歌曲标题中单次的频率分布。我们首先使用Stream.flatMap() 和Pattern.splitAsStream()来将歌曲的名称分割成不同的单词,并产生一个所有歌曲名字中单词的流。然后我们可以使用groupingBy作为分类函数,该函数以String.toUpperCase分类(这样所有相同的单词,忽略大小写,都被认为是相同的,并放入同一桶位中),再使用count()收集器作为下游收集器来计算每个单词出现的次数(没有直接创建一个集合):
Pattern pattern = Pattern.compile(\\s+");
Map<String, Integer> wordFreq =
tracks.stream()
.flatMap(t -> pattern.splitAsStream(t.name)) // Stream<String>
.collect(groupingBy(s -> s.toUpperCase(),
counting()));
flatMap()方法接受一个函数作为参数,该函数映射输入元素(这里是歌曲)到某类流中(这里是歌曲名字的单词)。它将映射函数应用到流中的每一个元素,然后用结果流中的内容替换每一个元素。(将这一过程分为两个操作,首先将每一个元素映射到一个流中,然后将结果流中的所有内容整合到一个流中)那么这里,flatMap操作的结果是一个包含了所有歌曲的名称单词的流。然后我们将单词分组到包含出现的单词的桶位中,最后使用counting()收集器来计算每个桶位中单词的个数。
Collectors类有许多构造不同收集器的方法,这些收集器可以用于所有常见的查询,统计,制表,并且你也可以实现自己的收集器。
9,并行的底层原理
随着Java SE 7中加入了Fork/Join框架,JDK中便有了高效实现的并行计算API。然而,使用Fork/Join的并行代码与同等功能的串行代码看起来非常不同(并且更大),这一直是并行化的一个障碍。通过在并行流和串行流上支持完全相同的操作,用户可以移除这一障碍,不需要重写代码便可在串行和并行之间自由切换,这使得并行更加容易并且不易出错。
通过递归分解实现一个并行计算所涉及的步骤包括:将一个问题分为一些子问题,解决子问题并产生部分结果,然后将这些部分结果合并。Fork/Join机制被设计为自动化这一过程。
为了支持在任意流数据源上的多有操作,我们将流数据源抽象为一个叫做Spliterator的模型,这是传统Iterator的一个广义化。为了支持顺序访问数据元素,Spliterator还支持分解:就像一个Iterator允许你切除单个元素并保持剩余元素依旧为Iterator,Spliterator允许你在输入元素上分割一个更大的数据块生成一个新的Spliterator,并切保持剩余的数据依旧是最初的Spliterator。(两个Spliterator依旧可以再分割)另外,Spliterator可以提供数据源的元数据像是元素个数(如果知道)以及一些列boolean值(像是‘元素是有序的’),这些用来优化Stream框架。
这一方法将递归分解的结构性质与在可分解的数据结构上进行并行计算的算法分离开来。数据结构的作者只需提供一个分界逻辑,然后就可以直接在流操作的并行计算上获益了。
大部分用户不需要实现Spliterator;他们只需要在集合上使用stream()方法即可。但是如果你曾经实现一个集合或其他流数据源,那么可能你想自定义一个Spliterator。Spliterator的API如下:
public interface Spliterator<T> {
// Element access
boolean tryAdvance(Consumer<? super T> action);
void forEachRemaining(Consumer<? super T> action);
// Decomposition
Spliterator<T> trySplit();
// Optional metadata
long estimateSize();
int characteristics();
Comparator<? super T> getComparator();
}
基本接口像Iterable和Collection提供了正确但低效的spliterator实现,但是子接口(如Set)和一些具体实现(如ArrayList)利用父接口不可用的信息实现了更加高效的spliterator。spliterator实现的质量将会影响流的执行性能;从split方法分割出均衡的结果会提高CPU的利用率,并且提供正确的元数据还会有其他优化。
10,出现顺序
许多数据源如列表,数组,以及I/O通道,都有一个自然的出现顺序,这意味着元素出现的顺序是有意义的。其他的数据源,像是HashSet则没有定义出现顺序(因此HashSet的Iterator允许以任意顺序提供元素)
Spliterator中维护的一个boolean属性,并用于流实现的就是当前流是否是定义出现顺序的。除了一些例外(如Stream.forEach()和Stream.findAny()),并行操作是被约束为需要出现顺序的。这意味着像下面的流管道:
List<String> names = people.parallelStream()
.map(Person::getName)
.collect(toList());
name必须与数据源中相应people保持相同顺序。通常这是我们想要的,并且对于许多流操作,这不需要额外的消耗。另一方面,如果数据源是HashSet,name可能会出现任意顺序,甚至并行操作下出现不同顺序。
11,JDK中的流和Lambda
将Stream暴露为一个顶级抽象模型,我们想要确保流的特性能尽可能的贯穿整个JDK,Collection已经被增强为通过stream()和parrallelStream()可以转换为流;数组可以通过Arrays.stream()转换为流。
另外Stream中有一些静态工厂方法(以及相关的基本类型具体实现)用来创建流,像是Stream.of,Stream.generate,和IntStream.range。许多其他类也有了一些流转换方法,像是String.chars,BufferedReader.lines,Pattern.splitAsStream,Random.ints和BitSet.stream。
最后,我们提供一些列被库作者使用的API来构造流,他们希望暴露的非标准的流聚合功能。用来创建流的最小的信息是一个Iterator,但是如果创建者有额外的元数据(如大小),库可以通过实现Spliterator来提供更加高效的流实现。
12,比较器工厂
Comparator 类中添加了许多新的用来构建比较器的方法。
静态方法Comparator.comparing()接受一个提取Comparable排序键的函数并生成一个Comparator。它的实现非常简单:
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) {
return (c1, c2)
-> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
这样的方法是高阶函数的一个例子---接受一个函数作为参数或者生成一个新的函数的函数。但是这种方法真正强大之处是它的更好的可组合性。举个例子,Comparator有一个默认的方法来翻转比较方向。那么,为了将people别表以lastname 反序排序,我们只需简单的创建一个之前的比较器,然后要求它翻转自己即可:
people.sort(comparing(p -> p.getLastName()).reversed());
同样的,默认方法thenComparing允许我们接受一个Comparator并且改善它的行为当最初的比较器视两个元素相同时。以lastname和firtname排序people列表,我们可以这样:
Comparator<Person> c = Comparator.comparing(p -> p.getLastName())
.thenComparing(p -> p.getFirstName());
people.sort(c);
13,可变集合操作
集合上的流操作生成一个新的值,集合,或者副作用。然而,有时我们就想改变原始集合,并且Collection,List,Map中已经添加了一些新方法来利用Lambda表达式,像是Iterable.forEach(Consumer),Collection.removeAll(Predicate),List.replaceAll(UnaryOperator),List.sort(Comparator),以及Map.computeIfAbsent()。另外,ConcurrentMap中的非原子版本的方法,如replace和putIfAbsent已经从Map中去除了。
14,总结
向语言中添加Lambda表达式是一个巨大的进步,开发者每天都在使用核心库来完整他们的工作,因此语言的烟花需要配合核心库的演化以便用户可以尽快使用新特性。新标准库的中心特性就是Stream的概念,这为我们提供了强大的基于数据集的聚合操作工具,并且已经深度整合到当前的集合类以及其他JDK类中来了。