【Java没基础】函数式编程——Stream API 中的收集器

来源:互联网 发布:好听的网络名字 男孩 编辑:程序博客网 时间:2024/06/01 07:31

前言

这是 Java 8 函数式编程系列的第二篇blog。在上篇,我们对 Java 8 的语法糖 Lambda 表达式以及部分 Stream API 进行了基本的学习,为了便于理解,我们写了一些简单的 Demo。详情请查看上一篇blog:【Java没基础】JDK1.8 中的 Lambda 表达式与 Stream API。
在今天的这篇blog中,我们来总结学习一下 Stream API 中更高级的应用。

方法引用

我们都说 Lambda 表达式是 Java 8 给程序员们提供的语法糖,那么 Java 8 也为 Lambda 表达式提供了语法糖,能把 Lambda 表示式变得更简单,叫做 方法引用,这可以帮助程序员更方便的重用已有方法。

// 伪代码,为了更加容易理解String userName = user.getName();  |-- 加糖 user -> user.getName();    |-- 加糖 User :: getName;  __方法引用

如上:这是由一个普通的方法转换到使用 Lambda 表达式调用,然后使用方法引用来进行方法调用。

ClassName :: methodName 这是方法引用的标准语法。
虽然这同样是方法调用,但是不需要在后面加括号,因为这里并不调用该方法,这里只是提供了和 Lambda 表达式等价的一种结构,需要时才会调用。凡是使用 Lambda 表达式的地方,就可以使用方法引用。

对于构造函数,我们也可以使用同样的缩写形式,比如

// 伪代码,为了更加容易理解User user = new User(username, password);  |-- (username, password) -> new User(username, password)    |-- User :: new;

方法引用自动支持多个参数, 前提是选对了正确的函数接口。

同样的,我们也可以使用这种方式来创建一个数组

// 伪代码,为了更加容易理解String[] str = new String[length]; <==> String[] :: new;

凡是使用 Lambda 表达式的地方,就可以使用方法引用。方法引用只不过是基于这样的事实,提供了一种简短的语法而已。 ——Java 8 函数式编程

元素顺序

我们都知道,在 Java 中,一些集合类的元素是有序的:比如 List 等,有一些集合类的元素是无序的:比如 Set。

直观上来说,流是有序的,因为流中的元素都是按顺序进行处理,这种顺序成为出现顺序,出现顺序的定义依赖于数据源和流的操作。

在一个有序的集合中创建一个流,流中的元素就会按照顺序排列,因此,有序集合流的顺序测试永远可以通过

import org.junit.Test;...List<Integer> numbers = asList(1, 2, 3, 4);List<Integer> sameOrder = numbers.stream.collect(toList());Assert.assertEquals(numbers, sameOrder); // 断言测试正确

而如果集合的元素是无序的,比如 Set,因此生成的流也是无序的,比如 HashSet,因此不能保证每次断言测试都能通过。

Set<Integer> numbers = new HashSet<>(asList(1, 2, 3, 4));List<Integer> sameOrder = numbers.stream.collect(toList());Assert.assertEquals(asList(1, 2, 3, 4), sameOrder); //不保证每次断言都通过

有些集合本身是无序的,但这些操作有时会产生顺序。
在流操作中,有些中间操作会产生顺序,比如对值映射时,映射后的值是有序的,这种顺序就会保存下来。如果进来的流是无序的,出去的流也是无序的。

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 方法不能保证元素是按照顺序处理的。如果需要保证按顺序处理, 应该使用 forEachOrdered 方法。

收集器

收集器:一种通用的,从流中生成复杂值的结构,其实也是方法的一种。只需要将收集器传给collect 方法,所有的流就可以使用它了。Java 8 的标准库中已经提供了一些有用的收集器,他们在 java.util.stream.Collectors 中被定义。

集合转换

先介绍一个常用的收集器:toCollection。有时候,我们可能希望使用TreeSet,而不是由框架在背后自动为你指定一种类型的Set,此时就可以使用 toCollection ,它接受一个函数做参数来创建集合。

// 用定制的集合收集参数stream.collect(toCollection(TreeSet :: new));

求值

Java 8 提供一些收集器用来求值,如 minBy(求最小值), maxBy(求最大值), averagingInt(求平均值), summingInt(求和)

public double averAgeOfUser(List<User> users) {  return users.stream()    .collect(averagingInt(user -> user.getAge().size()));}

通过调用 stream 方法让集合生成流, 然后调用 collect 方法收集结果。averagingInt 方法接受一个 Lambda 表达式作参数, 将流中的元素转换成一个整数, 然后再计算平均数。

分块和分组

对于一个集合,我们通常会有需求来集合中的元素进行分块或者分组操作。在 Java 8 之前,我们可能需要迭代这个集合,然后使用if语句、switch case 语句来对元素进行分组,而 Java 8 为我们提供了这样的两个收集器 partitioningBygroupingBy

分块

partitioningBy 接受一个流,并将其分为两部分。它使用 Predicate 对象判断元素应该属于哪个部分,将数据分为 true 和 false 两部分,并根据其布尔值返回一个Map

// 把用户按照性别进行分组public Map<boolean, List<User>> manOrWoman(Stream<User> users) {  return users.collect(partitioningBy(User :: isMan));}// 方法public boolean isMan(User user){  if ("男".equals(user.userSex)) {    return true;  } else {    return false;  }}
分组

groupingBy: 比 partitioningBy 更自然,可以使用任意值对数据进行分组。

// 将用户按照国家分组public Map<Country, List<User>> usersByCountry(Stream<User> users) {  return users.collect(groupingBy(User :: getCountry));}

字符串

对于一个集合,我们可能需要需要获取他某个字段的字符信息,在 Java 8 之前我们通常采用迭代器,在一些特殊的情景中我们可能还需要加上前后缀字符。在 J 8 之后,我们拥有了 joining 方法。

// 获取这个Stream流 users的用户名列表,并用[]来装载String result = users.stream().map(User :: getName).collect(Collectors.joining(",","[","]");// 这样,我们就能获得一个用户名列表字符串 [Lee, Jone, King] (举例)。

joining 可以方便的从一个流中得到一个字符串,并且可以提供分隔符,前缀与后缀。
它的一般格式是 Collectors.joining(“分隔符”, “前缀”, “后缀”).

组合收集器

对于不同的业务逻辑,我们对于集合的处理也会采用不同的方式,将收集器组合起来可以帮助我们完成更为复杂的功能。

在这里,我们来假设一个情景,然后思考一下应该怎样通过组合收集器的方式来实现。

// 计算每个城市中的注册汽车总数:先按照城市来进行分组,然后计算每个城市中的汽车数量public Map<City, Long> carNumbersOfCity(Stream<Car> cars) {  return cars.collect(groupingBy(car -> car.getCity(), // 先分组    counting())); // 再计数}

在上边的代码中,我们先使用 groupingBy 根据汽车注册的城市来进行分组,然后使用下游的另一个收集器 counting 来计算每组的元素数量,将结果映射为 Map.

接着我们来讨论另一个情景,如果我们不想获取到每个城市下注册的汽车数量,而是想获取到每个城市下注册的汽车的车主名列表呢( 简单举例,假设只有十几辆汽车注册在几个城市中,对于大数据量的优化我们以后来进行讨论 )。
我们能够想到,先分组,然后再按照分组来获取每个城市下注册车辆的车主名。但是我们会发现,这里不能直接使用流的 map 操作, 因为列表是由 groupingBy 生成的。 我们需要有一种方法, 可以告诉 groupingBy 将它的值做映射, 生成最终结果。

每个收集器都是生成最终值的一剂良方。 这里需要两剂配方, 一个传给另一个。 谢天谢地, Oracle 公司的研究员们已经考虑到这种情况, 为我们提供了 mapping 收集器。

mapping 允许在收集器的容器上执行类似 map 的操作。 但是需要指明使用什么样的集合类存储结果, 比如 toList。

// 获取每个城市下注册的汽车的车主名列表public Map<City, List<String>> driverOfCity(Stream<Car> cars) {  return cars.collect(groupingBy(Cars :: getCity,    mapping(Cars :: getDriver, toList())));}

我们都用到了第二个收集器, 用以收集最终结果的一个子集。 这些收集器叫作下游收集器。 收集器是生成最终结果的一剂配方, 下游收集器则是生成部分结果的配方,主收集器中会用到下游收集器。 这种组合使用收集器的方式, 使得它们在 Stream 类库中的作用更加强大。 —— Java 8 函数式编程

那些为基本类型特殊定制的函数, 如 averagingDouble、 summarizingLong 等, 事实上和调用特殊 Stream 上的方法是等价的, 加上它们是为了将它们当作下游收集器来使用的。

重构收集器

在 Java 类库中,为程序员们提供的收集器基本上完全可以满足项目需求。但是就像有时候需要重写 hashCode 和 equals 方法一样,收集器其实也是方法,那么我们也可以对收集器来进行重构。

具体可以参看 《Java 8 函数式编程》第五章重构和定制收集器一节。~(我也在研究之中)~

后记

对于Java的函数式编程中的 高级集合类与收集器 的一些话题我们先聊到这里,在下一篇blog中,我们来着重讨论一下并行流:这个大大提高流工作效率的技术。

在学习 Java 8 的函数式编程的过程中,我准备用三篇 blog 来对我的学习进行一个总结性的描述:
* JDK1.8 中的 Lambda 表达式与 Stream API
* Stream API 中的高级集合类与收集器
* Java 8 函数式编程 —— 并行流

这里有个傲娇的二维码,关注一下呗亲~
大数据与Java进阶

原创粉丝点击