当前位置: 首页 > 编程笔记 >

Java函数式编程(十):收集器

滕令雪
2023-03-14
本文向大家介绍Java函数式编程(十):收集器,包括了Java函数式编程(十):收集器的使用技巧和注意事项,需要的朋友参考一下

前面我们已经用过几次collect()方法来将Stream返回的元素拼成ArrayList了。这是一个reduce操作,它对于将一个集合转化成另一种类型(通常是一个可变的集合)非常有用。collect()函数,如果和Collectors工具类里的一些方法结合起来使用的话,能提供极大的便利性,本节我们将会介绍到。

我们还是继续使用前面的Person列表作为例子,来看一下collect()方法到底有哪些能耐。假设我们要从原始列表中找出所有大于20岁的人。下面是使用了可变性和forEach()方法实现的版本:


List<Person> olderThan20 = new ArrayList<>(); people.stream()

        .filter(person -> person.getAge() > 20)

.forEach(person -> olderThan20.add(person)); System.out.println("People older than 20: " + olderThan20);

我们使用filter()方法来从列表中过滤出了所有年龄大于20的人。然后,在forEach方法里,我们将元素添加到一个在前面已经初始化好的ArrayList中。我们先看下这段代码的输出结果,一会儿再去重构它。


People older than 20: [Sara - 21, Jane - 21, Greg - 35]

程序输出的结果是对的,不过还有点小问题。首先,把元素添加到集合中,这种属于低级操作——它是命令式的,而非声明式的。如果我们想把这个迭代改造成并发的,还得去考虑线程安全的问题——可变性使得它难以并行化。幸运的是,使用collect()方法可以很容易解决掉这个问题。来看下如何实现的。

collect()方法接受一个Stream并将它们收集到一个结果容器中。要完成这个工作,它需要知道三个东西:

+如何创建结果容器(比如说,使用ArrayList::new方法) +如何把单个元素添加到容器中(比如使用ArrayList::add方法) +如何把一个结果集合并到另一个中(比如使用ArrayList::addAll方法)

对于串行操作而言,最后一条不是必需的;代码设计的目标是能同时支持串行和并行的。

我们把这些操作提供给collect方法,让它来把过滤后的流给收集起来。


List<Person> olderThan20 =

people.stream()

.filter(person -> person.getAge() > 20)

.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

System.out.println("People older than 20: " + olderThan20);

这段代码的结果和前面一样,不过这样写有诸多好处。

首先,我们编程的方式更聚焦了,表述性也更强,清晰的传达了你要把结果收集到一个ArrayList里去的目的。collect()的第一个参数是一工厂或者生产者,后面的参数是一个用来收集元素的操作。

第二,由于我们没有在代码中个执行显式的修改操作,可以很容易并行地执行这个迭代。我们让底层库来完成修改操作,它自己会处理好协作及线程安全的问题,尽管ArrayList本身不是线程安全的——干的漂亮。

如果条件允许的话,collect()方法可以并行地将元素添加到不同的子列表中,然后再用一个线程安全的方式将它们合并到一个大列表里(最后一个参数就是用来进行合并操作的)。

我们已经看到,相对于手动把元素添加到列表而言,使用collect()方法的好处真是太多了。下面我们来看下这个方法的一个重载的版本——它更简单也更方便——它是使用一个Collector作为参数。这个Collector是一个包含了生产者,添加器,以及合并器在内的接口——在前面的版本中这些操作是作为独立的参数分别传入方法中的——使用Collector则更简单并且可以复用。Collectors工具类提供了一个toList方法,可以生成一个Collector的实现,用来把元素添加到ArrayList中。我们来修改下前面那段代码,使用一下这个collect()方法。


List<Person> olderThan20 =

people.stream()

.filter(person -> person.getAge() > 20)

.collect(Collectors.toList());

System.out.println("People older than 20: " + olderThan20);

使用了Collectors工具类的简洁版的collect()方法,可不止这一种用法。Collectors工具类中还有好几种不同的方法来可以进行不同的收集和添加的操作。比如说,除了toList()方法,还有toSet()方法,可以添加到一个Set中,toMap()方法可以用来收集到一个key-value的集合中,还有joining()方法,可以拼接成一个字符串。我们还可以将mapping(),collectingAndThen(),minBy(), maxBy()和groupingBy()等方法组合起来进行使用。

我们来用下groupingBy()方法来将人群按年龄进行分组。


Map<Integer, List<Person>> peopleByAge =

people.stream()

.collect(Collectors.groupingBy(Person::getAge));

System.out.println("Grouped by age: " + peopleByAge);

只需简单的调用下collect()方法便能完成分组。groupingBy()接受一个lambda表达式或者方法引用——这种叫分类函数——它返回需要分组的对象的某个属性的值。根据我们这个函数返回的值,来把调用上下文中的元素放进某个分组中。在输出中可以看到分组的结果:


Grouped by age: {35=[Greg - 35], 20=[John - 20], 21=[Sara - 21, Jane - 21]}

这些人已经按年龄进行了分组。

在前面这个例子中我们按人群的年龄对他们进行了分组收集。groupingBy()方法的一个变种可以按多个条件进行分组。简单的groupingBy()方法使用了分类器进行元素收集。而通用的groupingBy()收集器,则可以为每一个分组指定一个收集器。也就是说,元素在收集的过程中会途经不同的分类器和集合,下面我们将会看到。

继续使用上面这个例子,这回我们不按年龄分组了,我们只获取人的名字,按他们的年龄进行排序。


Map<Integer, List<String>> nameOfPeopleByAge =

people.stream()

.collect(

groupingBy(Person::getAge, mapping(Person::getName, toList())));

System.out.println("People grouped by age: " + nameOfPeopleByAge);

这个版本的groupingBy()接受两个参数:第一个是年龄,这是分组的条件,第二个是一个收集器,它是由mapping()函数返回的结果。这些方法都来自Collectors工具类,在这段代码中进行了静态的导入。mapping()方法接受两个参数,一个是映射用的属性,一个是对象要收集到的地方,比如说list或者set。来看下上面这段代码的输出结果:


People grouped by age: {35=[Greg], 20=[John], 21=[Sara, Jane]}

可以看到,人们的名字已经按年龄进行分组了。

我们再来看一个组合的操作:按名字的首字母进行分组,然后选出每个分组中年纪最大的那位。


Comparator<Person> byAge = Comparator.comparing(Person::getAge);

Map<Character, Optional<Person>> oldestPersonOfEachLetter =

people.stream()

.collect(groupingBy(person -> person.getName().charAt(0),

reducing(BinaryOperator.maxBy(byAge))));

System.out.println("Oldest person of each letter:");

System.out.println(oldestPersonOfEachLetter);

我们先是按名字的首字母进行了排序。为了实现这个,我们把一个lambda表达式作为groupingBy()的第一个参数传了进去。这个lambda表达式是用来返回名字的首字母的,以便进行分组。第二个参数不再是mapping()了,而是执行了一个reduce操作。在每个分组内,它使用maxBy()方法,从所有元素中递推出最年长的那位。由于组合了许多操作,这个语法看起来有点臃肿,不过整个读起来是这样的:按名字首字母进行分组,然后递推出分组中最年长的那位。来看下这段代码的输出,它列出了指定字母开头的那组名字中年纪最大的那个人。


Oldest person of each letter:

{S=Optional[Sara - 21], G=Optional[Greg - 35], J=Optional[Jane - 21]}

我们已经领教到了collect()方法以及Collectors工具类的威力。在你的IDE或者JDK的官方文档里面,再花点时间去研究下Collectors工具类吧,熟悉下它提供的各种方法。下面我们将会用lambda表达式来完成一些过滤器的实现。

 类似资料:
  • 本文向大家介绍Java函数式编程(十一):遍历目录,包括了Java函数式编程(十一):遍历目录的使用技巧和注意事项,需要的朋友参考一下 列出目录中的文件 用File类的list()方法可以很容易的列出目录中的所有文件的文件名。如果想要获取文件而不止是文件名的话,可以使用它的listFiles()方法。这很简单,难的是怎么去处理这个返回的列表。我们不再使用传统的冗长的外部迭代器,而是使用优雅的函数式

  • 问题内容: 如何在Java中模拟函数式编程,特别是如何将函数映射到项目集合? 什么是最冗长和尴尬的方法? 问题答案: 在Java之前,所有的函数式编程尝试在Java中都会有些 冗长 和/或 笨拙 ,直到Java 8。 最 直接的 方法是提供一个接口(例如Guava的这种形式),并提供采用和调用该接口的各种方法(例如我认为您的方法应该执行的操作)。 不好的事情是,您需要使用匿名内部类来实现并经常这样

  • 本文向大家介绍Java函数式编程(十二):监控文件修改,包括了Java函数式编程(十二):监控文件修改的使用技巧和注意事项,需要的朋友参考一下 使用flatMap列出子目录 前面已经看到如何列出指定目录下的文件了。我们再来看下如何遍历指定目录的直接子目录(深度为1),先实现一个简单的版本,然后再用更方便的flatMap()方法来实现。 我们先用传统的for循环来遍历一个指定的目录。如果子目录中有文

  • 本文向大家介绍Java函数式编程(二):集合的使用,包括了Java函数式编程(二):集合的使用的使用技巧和注意事项,需要的朋友参考一下 第二章:集合的使用 我们经常会用到各种集合,数字的,字符串的还有对象的。它们无处不在,哪怕操作集合的代码要能稍微优化一点,都能让代码清晰很多。在这章中,我们探索下如何使用lambda表达式来操作集合。我们用它来遍历集合,把集合转化成新的集合,从集合中删除元素,把集

  • 本文向大家介绍详解JAVA 函数式编程,包括了详解JAVA 函数式编程的使用技巧和注意事项,需要的朋友参考一下 1.函数式接口 1.1概念: java中有且只有一个抽象方法的接口。 1.2格式: 1.3@FunctionalInterface注解: 与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注 解可用于

  • 本文向大家介绍Java函数式编程(六):Optional,包括了Java函数式编程(六):Optional的使用技巧和注意事项,需要的朋友参考一下 选取单个元素 直觉来说选取单个元素肯定会比选取多个要简单得多,不过这里也存在一些问题。我们先看下一般的做法的问题是什么,然后再看下如何用lambda表达式来解决它。 我们先新建一个方法来查找一个以特定字母开头的元素,然后打印出来。 这个方法简直跟刚过去