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表达式来完成一些过滤器的实现。

时间: 2024-11-01 00:58:52

Java函数式编程(十):收集器_java的相关文章

Java函数式编程(十二):监控文件修改_java

使用flatMap列出子目录 前面已经看到如何列出指定目录下的文件了.我们再来看下如何遍历指定目录的直接子目录(深度为1),先实现一个简单的版本,然后再用更方便的flatMap()方法来实现. 我们先用传统的for循环来遍历一个指定的目录.如果子目录中有文件,就添加到列表里:否则就把子目录添加到列表里.最后,打印出所有文件的总数.代码在下面--这个是困难模式的. 复制代码 代码如下: public static void listTheHardWay() {      List<File> f

Java函数式编程(五):闭包_java

使用词法作用域和闭包 很多开发人员都存在这种误解,认为使用lambda表达式会导致代码冗余,降低代码质量.恰恰相反,就算代码变得再复杂,我们也不会为了代码的简洁性而在代码质量上做任何妥协,下面我们就会看到. 在前面一个例子中我们已经可以重用lambda表达式了;然而,如果再匹配另外一个字母,代码冗余的问题很快又卷土重来了.我们先来进一步分析下这个问题,然后再用词法作用域和闭包来把它解决掉. lambda表达式带来的冗余 我们来从friends中过滤出那些以N或者B开头的字母.继续延用上面的那个例

Java函数式开发 Optional空指针处理_java

摘要 空闲时会抽空学习同在jvm上运行的Groovy和Scala,发现他们对null的处理比早期版本Java慎重很多.在Java8中,Optional为函数式编程的null处理给出了非常优雅的解决方案.本文将说明长久以来Java中对null的蹩脚处理,然后介绍使用Optional来实现Java函数式编程. 那些年困扰着我们的null 在Java江湖流传着这样一个传说:直到真正了解了空指针异常,才能算一名合格的Java开发人员.在我们逼格闪闪的java码字符生涯中,每天都会遇到各种null的处理,

Java函数式编程(一):你好,Lambda表达式_java

第一章 你好,lambda表达式! 第一节 Java的编码风格正面临着翻天覆地的变化. 我们每天的工作将会变成更简单方便,更富表现力.Java这种新的编程方式早在数十年前就已经出现在别的编程语言里面了.这些新特性引入Java后,我们可以写出更简洁,优雅,表达性更强,错误更少的代码.我们可以用更少的代码来实现各种策略和设计模式. 在本书中我们将通过日常编程中的一些例子来探索函数式风格的编程.在使用这种全新的优雅的方式进行设计编码之前,我们先来看下它到底好在哪里. 改变了你的思考方式 命令式风格--

Java函数式编程(三):列表的转化_java

列表的转化 将集合转化成一个新的集合就和遍历它一样简单.假设我们要将列表中的名字转化成全大写的.我们看下都有哪些实现方式. Java中的字符串是不可变的,所以它没法改变.我们可以生成新的字符串,用来替换列表中原有的元素.然而这样做的话,原来列表就没了;还有一个问题,原来的列表可能也是不可变的,比如Arrays.asList()生成的,所以修改原来的列表这招不行.还有一个缺点就是这样做很难并行操作. 生成一个新的全大写的列表是个不错的选择. 乍听起来这个建议弱爆了;性能是我们都很关注的一个问题.令

Java函数式编程(八):字符串及方法引用_java

第三章 字符串,比较器和过滤器 JDK引入的一些方法对写出函数式风格的代码很有帮助.JDK库里的一些的类和接口我们已经用得非常熟悉了,比如说String,为了摆脱以前习惯的那种老的风格,我们得主动寻找机会来使用这些新的方法.同样,当我们需要用到只有一个方法的匿名内部类时,我们现在可以用lambda表达式来替换它了,不用再像原来那样写的那么繁琐了. 本章我们会使用lambda表达式和方法引用来遍历字符串,实现Comparator接口,查看目录中的文件,监视文件及目录的变更.上一章中介绍的一些方法还

Java函数式编程(二):集合的使用_java

第二章:集合的使用 我们经常会用到各种集合,数字的,字符串的还有对象的.它们无处不在,哪怕操作集合的代码要能稍微优化一点,都能让代码清晰很多.在这章中,我们探索下如何使用lambda表达式来操作集合.我们用它来遍历集合,把集合转化成新的集合,从集合中删除元素,把集合进行合并. 遍历列表 遍历列表是最基本的一个集合操作,这么多年来,它的操作也发生了一些变化.我们使用一个遍历名字的小例子,从最古老的版本介绍到现在最优雅的版本. 用下面的代码我们很容易创建一个不可变的名字的列表: 复制代码 代码如下:

Java 8必将掀起Java函数式编程热潮

Java 8给Java带来了一场变革.很明显,这个版本是过去十年以来推出的最具份量的Java更新,其中囊括了海量新特性,包括默认方法.方法与构造函数引用以及Lambda函数等等. 其中最有趣的一项特性当数全新java.util.streamAPI,它作为Javadoc状态存在,能够对元素流进行函数式操作,例如在集合中进行map-reduce变换. 将这个新API与Lambda表达式相结合,我们就得到了一条简洁但却强大的语法,能够对应用程序中的代码进行大幅简化. 就以表面上看起来相当简单的集合过滤

基于范型的java函数式编程(一)

编程|函数 注:在您阅读本篇的时候,希望你对Java Generic(范型)能够有所了解和明白. 记:周末在给javaparty讲FP中,很多人似乎对fp并不关心,也认为java中fp的作用不大.其实这是个很大的观念错误,范型的发展,对java的函数式编程支持很大,对Functor的影响也非常大.Functor在算法.逻辑.条件计算.规则引擎等等方面,都会有很大的作为,这个影响可就会深远的多了.-- 估且以此篇的开端,唤醒java开发者对FP in Java的重新认识. 周六给javaparty