Java Stream API进阶篇

本文github地址html

上一节介绍了部分Stream常见接口方法,理解起来并不困难,但Stream的用法不止于此,本节咱们将仍然以Stream为例,介绍流的规约操做。java

规约操做(reduction operation)又被称做折叠操做(fold),是经过某个链接动做将全部元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将全部元素转换成一个列表或集合,都属于规约操做。Stream类库有两个通用的规约操做reduce()collect(),也有一些为简化书写而设计的专用规约操做,好比sum()max()min()count()等。git

最大或最小值这类规约操做很好理解(至少方法语义上是这样),咱们着重介绍reduce()collect(),这是比较有魔法的地方。github

多面手reduce()

reduce操做能够实现从一组元素中生成一个值,sum()max()min()count()等都是reduce操做,将他们单独设为函数只是由于经常使用。reduce()的方法定义有三种重写形式:编程

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • T reduce(T identity, BinaryOperator<T> accumulator)
  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

虽然函数定义愈来愈长,但语义未曾改变,多的参数只是为了指明初始值(参数identity),或者是指定并行执行时多个部分结果的合并方式(参数combiner)。reduce()最经常使用的场景就是从一堆值中生成一个值。用这么复杂的函数去求一个最大或最小值,你是否是以为设计者有病。其实否则,由于“大”和“小”或者“求和"有时会有不一样的语义。api

需求:从一组单词中找出最长的单词。这里“大”的含义就是“长”。数组

// 找出最长的单词![](http://images2015.cnblogs.com/blog/939998/201703/939998-20170314192638495-351834305.png)

Stream<String> stream = Stream.of("I", "love", "you", "too");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional<String> longest = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println(longest.get());

上述代码会选出最长的单词love,其中Optional是(一个)值的容器,使用它能够避免null值的麻烦。固然可使用Stream.max(Comparator<? super T> comparator)方法来达到同等效果,但reduce()自有其存在的理由。oracle

Stream.reduce_parameter

需求:求出一组单词的长度之和。这是个“求和”操做,操做对象输入类型是String,而结果类型是Integerapp

// 求单词长度之和
Stream<String> stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0, // 初始值 // (1)
        (sum, str) -> sum+str.length(), // 累加器 // (2)
        (a, b) -> a+b); // 部分和拼接器,并行执行时才会用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);

上述代码标号(2)处将i. 字符串映射成长度,ii. 并和当前累加和相加。这显然是两步操做,使用reduce()函数将这两步合二为一,更有助于提高性能。若是想要使用map()sum()组合来达到上述目的,也是能够的。ide

reduce()擅长的是生成一个值,若是想要从Stream生成一个集合或者Map等复杂的对象该怎么办呢?终极武器collect()横空出世!

>>> 终极武器collect() <<<

不夸张的讲,若是你发现某个功能在Stream接口中没找到,十有八九能够经过collect()方法实现。collect()Stream接口方法中最灵活的一个,学会它才算真正入门Java函数式编程。先看几个热身的小例子:

// 将Stream转换成容器或Map
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
// Set<String> set = stream.collect(Collectors.toSet()); // (2)
// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); // (3)

上述代码分别列举了如何将Stream转换成ListSetMap。虽然代码语义很明确,但是咱们仍然会有几个疑问:

  1. Function.identity()是干什么的?
  2. String::length是什么意思?
  3. Collectors是个什么东西?

接口的静态方法和默认方法

Function是一个接口,那么Function.identity()是什么意思呢?这要从两方面解释:

  1. Java 8容许在接口中加入具体方法。接口中的具体方法有两种,default方法和static方法,identity()就是Function接口的一个静态方法。
  2. Function.identity()返回一个输出跟输入同样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

上面的解释是否是让你疑问更多?不要问我为何接口中能够有具体方法,也不要告诉我你以为t -> tidentity()方法更直观。我会告诉你接口中的default方法是一个无奈之举,在Java 7及以前要想在定义好的接口中加入新的抽象方法是很困难甚至不可能的,由于全部实现了该接口的类都要从新实现。试想在Collection接口中加入一个stream()抽象方法会怎样?default方法就是用来解决这个尴尬问题的,直接在接口中实现新加入的方法。既然已经引入了default方法,为什么再也不加入static方法来避免专门的工具类呢!

方法引用

诸如String::length的语法形式叫作方法引用(method references),这种语法用来替代某些特定形式Lambda表达式。若是Lambda表达式的所有内容就是调用一个已有的方法,那么能够用方法引用来替代Lambda表达式。方法引用能够细分为四类:

方法引用类别 举例
引用静态方法 Integer::sum
引用某个对象的方法 list::add
引用某个类的方法 String::length
引用构造方法 HashMap::new

咱们会在后面的例子中使用方法引用。

收集器

相信前面繁琐的内容已完全打消了你学习Java函数式编程的热情,不过很遗憾,下面的内容更繁琐。

<img src="http://images2015.cnblogs.com/blog/939998/201703/939998-20170314192733276-1662918719.png" width="500px", align="right" alt="Stream.collect_parameter" />

收集器(Collector)是为Stream.collect()方法量身打造的工具接口(类)。考虑一下将一个Stream转换成一个容器(或者Map)须要作哪些工做?咱们至少须要两样东西:

  1. 目标容器是什么?是ArrayList仍是HashSet,或者是个TreeMap
  2. 新元素如何添加到容器中?是List.add()仍是Map.put()

若是并行的进行规约,还须要告诉collect() 3. 多个部分结果如何合并成一个。

结合以上分析,collect()方法定义为<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner),三个参数依次对应上述三条分析。不过每次调用collect()都要传入这三个参数太麻烦,收集器Collector就是对这三个参数的简单封装,因此collect()的另外一定义为<R,A> R collect(Collector<? super T,A,R> collector)Collectors工具类可经过静态方法生成各类经常使用的Collector。举例来讲,若是要将Stream规约成List能够经过以下两种方式实现:

// 将Stream规约成List
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);

一般状况下咱们不须要手动指定collect()的三个参数,而是调用collect(Collector<? super T,A,R> collector)方法,而且参数中的Collector对象大都是直接经过Collectors工具类得到。实际上传入的收集器的行为决定了collect()的行为

使用collect()生成Collection

前面已经提到经过collect()方法将Stream转换成容器的方法,这里再汇总一下。将Stream转换成ListSet是比较常见的操做,因此Collectors工具已经为咱们提供了对应的收集器,经过以下代码便可完成:

// 将Stream转换成List或Set
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
Set<String> set = stream.collect(Collectors.toSet()); // (2)

上述代码可以知足大部分需求,但因为返回结果是接口类型,咱们并不知道类库实际选择的容器类型是什么,有时候咱们可能会想要人为指定容器的实际类型,这个需求可经过Collectors.toCollection(Supplier<C> collectionFactory)方法完成。

// 使用toCollection()指定规约容器的类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)

上述代码(3)处指定规约结果是ArrayList,而(4)处指定规约结果为HashSet。一切如你所愿。

使用collect()生成Map

前面已经说过Stream背后依赖于某种数据源,数据源能够是数组、容器等,但不能是Map。反过来从Stream生成Map是能够的,但咱们要想清楚Mapkeyvalue分别表明什么,根本缘由是咱们要想清楚要干什么。一般在三种状况下collect()的结果会是Map

  1. 使用Collectors.toMap()生成的收集器,用户须要指定如何生成Mapkeyvalue
  2. 使用Collectors.partitioningBy()生成的收集器,对元素进行二分区操做时用到。
  3. 使用Collectors.groupingBy()生成的收集器,对元素作group操做时用到。

状况1:使用toMap()生成的收集器,这种状况是最直接的,前面例子中已提到,这是和Collectors.toCollection()并列的方法。以下代码展现将学生列表转换成由<学生,GPA>组成的Map。很是直观,无需多言。

// 使用toMap()统计学生GPA
Map<Student, Double> studentToGPA =
     students.stream().collect(Collectors.toMap(Functions.identity(),// 如何生成key
                                     student -> computeGPA(student)));// 如何生成value

状况2:使用partitioningBy()生成的收集器,这种状况适用于将Stream中的元素依据某个二值逻辑(知足条件,或不知足)分红互补相交的两部分,好比男女性别、成绩及格与否等。下列代码展现将学生分红成绩及格或不及格的两部分。

// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream()
         .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

状况3:使用groupingBy()生成的收集器,这是比较灵活的一种状况。跟SQL中的group by语句相似,这里的groupingBy()也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map的同一个key上。下列代码展现将员工按照部门进行分组:

// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));

以上只是分组的最基本用法,有些时候仅仅分组是不够的。在SQL中使用group by是为了协助其余查询,好比1. 先将员工按照部门分组,2. 而后统计每一个部门员工的人数。Java类库设计者也考虑到了这种状况,加强版的groupingBy()可以知足这种需求。加强版的groupingBy()容许咱们对元素分组以后再执行某种运算,好比求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫作上游收集器,以后执行其余运算的收集器叫作下游收集器(downstream Collector)。

// 使用下游收集器统计每一个部门的人数
Map<Department, Integer> totalByDept = employees.stream()
                    .collect(Collectors.groupingBy(Employee::getDepartment,
                                                   Collectors.counting()));// 下游收集器

上面代码的逻辑是否是越看越像SQL?高度非结构化。还有更狠的,下游收集器还能够包含更下游的收集器,这毫不是为了炫技而增长的把戏,而是实际场景须要。考虑将员工按照部门分组的场景,若是咱们想获得每一个员工的名字(字符串),而不是一个个Employee对象,可经过以下方式作到:

// 按照部门对员工分布组,并只保留员工的名字
Map<Department, List<String>> byDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                        Collectors.mapping(Employee::getName,// 下游收集器
                                Collectors.toList())));// 更下游的收集器

若是看到这里你尚未对Java函数式编程失去信心,恭喜你,你已经顺利成为Java函数式编程大师了。

使用collect()作字符串join

这个确定是你们喜闻乐见的功能,字符串拼接时使用Collectors.joining()生成的收集器,今后告别for循环。Collectors.joining()方法有三种重写形式,分别对应三种不一样的拼接方式。无需多言,代码过目难忘。

// 使用Collectors.joining()拼接字符串
Stream<String> stream = Stream.of("I", "love", "you");
//String joined = stream.collect(Collectors.joining());// "Iloveyou"
//String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"

collect()还能够作更多

除了可使用Collectors工具类已经封装好的收集器,咱们还能够自定义收集器,或者直接调用collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)方法,收集任何形式你想要的信息。不过Collectors工具类应该能知足咱们的绝大部分需求,手动实现之间请先看看文档。

本文github地址

参考文献

  1. https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#package.description
  2. https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
  3. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html
  4. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
  5. https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html
相关文章
相关标签/搜索