(翻译)Java SE 8 Lambda 标准库概览(下)

Wesley13
• 阅读 589

Java SE 8 Lambda 标准库概览

8,Collectors

    在目前的例子中,我们已经使用了collect()方法来收集流中的元素并放入List或Set中了。collec()方法的参数是一个收集器,它包含了收集或汇总多个元素到一个数据结构中的概念。Collectors 类提供了诸多常用收集器的工厂方法;toList()和toSet()是最常用的两种,但是还有很多可以用来执行复杂转换的收集器。

收集器的输入和输出均为参数化的。toList() 的 收集器接受一个T类型的输入并产生一个List类型的输出。一个稍微复杂一点的收集器是toMap,其中有多个版本。最简单的版本接受一对函数,一个用来映射键,另一个用来映射值。它接受一个T作为输入并产生一个Map<K, V>类型的输出,其中K和V键值映射函数的结果类型。(更加复杂的版本允许我们自定义输出结果的类型,或者解决多个元素映射同一个键时的重复)举个例子,创建一个已知唯一键的反向索引,如catalog number:

Map<Integer, Album> albumsByCatalogNumber =
    albums.stream()
          .collect(Collectors.toMap(a -> a.getCatalogNumber(), a -> a));

    与toMap相关的是groupingBy。假如我们想要将最喜欢的歌曲以歌手分组制成表格。我们希望有一个接受歌曲(Track)为输入,Map<Artist, List>为输出的收集器。这切好与groupingBy的简单形式的行为像匹配,它接受一个分类函数,并根据该函数生成一个映射键,该键的对应值是一组与键相关的输入元素。

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类中来了。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这