Java函数式编程(七):MapReduce_java

译注:map(映射)和reduce(归约,化简)是数学上两个很基础的概念,它们很早就出现在各类的函数编程语言里了,直到2003年Google将其发扬光大,运用到分布式系统中进行并行计算后,这个组合的名字才开始在计算机界大放异彩(那些函数式粉可能并不这么认为)。本文我们会看到Java 8在摇身一变支持函数式编程后,map和reduce组合的首次亮相(这里只是初步介绍,后续还会有针对它们的专题)。

对集合进行归约

现在为止我们已经介绍了几个操作集合的新技巧了:查找匹配元素,查找单个元素,集合转化。这些操作有一个共同点,它们都是对集合中的单个元素进行操作。不需要对元素进行比较,或者对两个元素进行运算。本节中我们来看一下如何比较元素,以及在遍历集合过程中动态维护一个运算结果。

我们先从简单的例子开始,然后再循序渐进。在第一个例子中,我们先来遍历一下friends集合,计算出所有名字的总字符数。

复制代码 代码如下:

System.out.println("Total number of characters in all names: " + friends.stream()
         .mapToInt(name -> name.length())
         .sum());

要算出所有字符的总数我们得知道每个名字的长度。通过mapToInt()方法可以轻松的完成这个。当我们已经把名字转化成了对应的长度之后,最后只需要把它们加到一块就行了。我们有一个内置的sum()方法来完成这个。下面是最后的输出:

复制代码 代码如下:

Total number of characters in all names: 26

我们使用了map操作的一个变种,mapToInt()方法(这种的有mapToInt, mapToDouble等,会对应生成具体类型的流,比如IntStream,DoubleStream),然后根据返回的长度计算出总的字符数。

除了使用sum方法,还有很多类似的方法可以使用,比如用max()可以求出最大的长度,用min()是最小长度,sorted()对长度进行排序,average()求平均长度,等等。

上述这个例子还有一个吸引人的地方就是现在越来越流行的MapReduce模式,map()方法进行映射,而sum()方法是一个比较常用的reduce操作。事实上,JDK中sum()方法的实现用的就是reduce()方法。我们来看下reduce操作更常用的一些形式。

比方说,我们遍历所有的名字,然后打印出名字最长的那个。如果最长的名字有好几个,我们就打印出最开始找到的那个。一种方法是,我们计算出最大的长度,然后选出匹配这个长度的第一个元素。不过这样做需要遍历两次列表——效率太低了。这正是reduce操作上场的时候了。

我们可以用reduce操作来比较两个元素的长度,然后返回最长的那个,再和剩下的元素做进一步比较。跟我们之前看到的别的高阶函数一样,reduce()方法同样也是遍历了整个集合。除此之外,它还记录了lambda表达式返回的计算结果。有个例子的话可以帮助我们更好的理解这点,那我们先来看一段代码吧。

复制代码 代码如下:

final Optional<String> aLongName = friends.stream()
         .reduce((name1, name2) ->
            name1.length() >= name2.length() ? name1 : name2);
aLongName.ifPresent(name ->
System.out.println(String.format("A longest name: %s", name)));

传给reduce()方法的lambda表达式接收两个参数,name1和name2,它会比较它们的长度,返回最长的那个。reduce()方法根本不知道我们要干什么。这个逻辑被剥离到我们传递进去的lambda表达式里面了——这是策略模式的一个轻量级的实现。

这个lambda表达式正好能适配成JDK中一个BinaryOperator的函数式接口的apply方法。这正是reduce方法要接受的参数类型。我们来运行下这个reduce方法,看看它能否正确地在两个最长的名字中选出第一个来。

复制代码 代码如下:

A longest name: Brian

在reduce()方法遍历集合的过程中,它先对集合的前两个元素调用了lambda表达式,调用返回的结果继续用于下一次调用。在第二次调用中,name1的值被绑定成上次调用的结果,name2的值则是集合的第三个元素。剩余的元素也这样依次调用下去。最后一次lambda表达式调用的结果,就是整个reduce()方法返回的结果。

reduce()方法返回的是一个Optional值,因为传递给它的集合可能是空的。那样的话,也不存在什么最长的名字了。如果列表只有一个元素,reduce方法直接返回那个元素,不会对lambda表达式进行调用。

从这个例子中我们可以推断出,reduce的结果最多只可能是集合中的一个元素。如果我们希望能返回一个默认值或者基础值的话,我们可以使用reduce()方法的一个变种,它可以接收一个额外的参数。比如,如果最短的名字是Steve,我们可以把它传给reduce()方法,像这样:

复制代码 代码如下:

final String steveOrLonger = friends.stream()
     .reduce("Steve", (name1, name2) ->
            name1.length() >= name2.length() ? name1 : name2);

如果有名字比它长的,那么这个名字会被选中;否则的话就返回这个基础值Steve。这个版本的reduce()方法不会返回Optional对象,因为如果集合是空的,会返回一个默认值;不用考虑没有返回值的情况。

在我们结束这章之前 ,我们再来看一下集合操作里面一个很基础的却又不是那么容易的操作:合并元素。

合并元素

我们已经学习了如何进行元素的查找,遍历,以及集合的转化。不过还有一个常见的操作——将集合元素进行拼接——如果没有这个新添加的join()函数的话,之前说的简洁和优雅的代码只能成为泡影了。这个简单的方法非常实用以至于它成为JDK里最常用的函数之一。我们来看下如何用它来打印列表中的元素,用逗号进行分隔。

我们还是用这个friends列表。如果用JDK库里的旧方法的话,想要打印出所有名字并用逗号隔开的话,要做哪些工作?

我们得遍历列表并且挨个打印元素。Java 5中的for循环比之前的有所改进,我们就用它吧。

复制代码 代码如下:

for(String name : friends) {
      System.out.print(name + ", ");
}
System.out.println();

代码很简单,我们看下它的输出是什么。

复制代码 代码如下:

Brian, Nate, Neal, Raju, Sara, Scott,

该死,最后多出了一个讨厌的逗号(我们难道要怪最后的那个Scott?)。怎么能让Java别放一个逗号在这呢?不幸的是,循环会按步就班的执行,想让它在最后特殊处理一下可不容易。为了解决这个问题,我们可以用回原来的那种循环方式。

复制代码 代码如下:

for(int i = 0; i < friends.size() - 1; i++) {
      System.out.print(friends.get(i) + ", ");
}
if(friends.size() > 0)
      System.out.println(friends.get(friends.size() - 1));

我们来看下这个版本的输出是不是OK。

复制代码 代码如下:

Brian, Nate, Neal, Raju, Sara, Scott

结果还是不错的,不过这个代码就不敢恭维了。救救我们吧,Java。

我们不用再忍受这种痛苦了。Java 8里的StringJoiner类帮我们搞定了这些难题,不止如此,String类还增加了一个join方法方便我们可以用一行代码来替代掉上面那坨东西。

复制代码 代码如下:

System.out.println(String.join(", ", friends));

快来看下吧,结果跟代码一样令人满意。

复制代码 代码如下:

Brian, Nate, Neal, Raju, Sara, Scott

结果还是不错的,不过这个代码就不敢恭维了。救救我们吧,Java。

我们不用再忍受这种痛苦了。Java 8里的StringJoiner类帮我们搞定了这些难题,不止如此,String类还增加了一个join方法方便我们可以用一行代码来替代掉上面那坨东西。

复制代码 代码如下:

System.out.println(String.join(", ", friends));

快来看下吧,结果跟代码一样令人满意。

复制代码 代码如下:

Brian, Nate, Neal, Raju, Sara, Scott

在底层实现中,String.join()方法调用了StringJoiner类来将第二个参数传进来的值(这是个变长参数)拼接成一个长的字符串,用第一个参数作为分隔符。这个方法当然不止是能拼接逗号这么简单了。比如说,我们可以传入一堆路径,然后很容易的拼出一个类路径(classpath),这可真是多亏了这些新增加的方法和类。

我们已经知道如何去连接列表元素了,在进行列表连接前,我们还可以先对元素进行转化,当然我们也知道如何使用map方法来进行列表转化了。接下来还可以用filter()方法过滤出我们想要的那些元素。最后一步的连接列表元素,用逗号还是什么分隔符,不过就是一个简单的reduce操作而已了。

我们可以用reduce()方法将元素拼接成一个字符串,不过这需要我们费点工夫。JDK有一个十分方便的collect()方法,它也是reduce()的一个变种,我们可以用它来把元素合并成一个想要的值。

collect()方法来执行归约操作,不过它把具体的操作委托给一个collector来执行。我们可以把转化后的元素合并成一个ArrayList。继续刚才那个例子,我们可以将转化后的元素,拼接成一个用逗号分隔的字符串。

复制代码 代码如下:

System.out.println(
      friends.stream()
          .map(String::toUpperCase)
          .collect(joining(", ")));

我们在转化后的列表上调用了collect()方法,给它传入了一个joining()方法返回的collector,joining是Collectors工具类里的一个静态方法。collector就像是个接收器,它接收collect传进来的对象,并把它们存储成你想要的格式:ArrayList, String等。我们会在52页的collect方法及Collectors类中进一步探索这个方法。

这是输出的名字,现在它们是大写的,并用逗号隔开。

复制代码 代码如下:

BRIAN, NATE, NEAL, RAJU, SARA, SCOTT

总结

集合在编程中十分常见,有了lambda表达式后,Java的集合操作变得更加简单容易了。那些拖沓的集合操作的老代码都可以换成这种优雅简洁的新方式。内部迭代器使得集合遍历,转化都变得更加方便,远离了可变性的烦恼,查找集合元素也变得异常轻松。使用这些新方法可以少写不少代码。这使得代码更容易维护,更聚焦于业务逻辑,编程中的那些基本操作也变得更少了。

下一章中我们会看到lambda表达式如何简化程序开发中的另一个基本操作:字符串操作以及对象比较。

时间: 2024-09-15 06:26:26

Java函数式编程(七):MapReduce_java的相关文章

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

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

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函数式编程(九):Comparator_java

实现Comparator接口 Comparator接口的身影在JDK库中随处可见,从查找到排序,再到反转操作,等等.Java 8里它变成了一个函数式接口,这样的好处就是我们可以使用流式语法来实现比较器了. 我们用几种不同的方式来实现一下Comparator,看看新式语法的价值所在.你的手指头会感谢你的,不用实现匿名内部类少敲了多少键盘啊. 使用Comparator进行排序 下面这个例子将使用不同的比较方法,来将一组人进行排序.我们先来创建一个Person的JavaBean. 复制代码 代码如下:

Java函数式编程(四):在集合中查找元素_java

查找元素 现在我们对这个设计优雅的转化集合的方法已经不陌生了,但它对查找元素却也是无能为力.不过filter方法却是为这个而生的. 我们现在要从一个名字列表中,取出那些以N开头的名字.当然可能一个也没有,结果可能是个空集合.我们先用老方法实现一把. 复制代码 代码如下: final List<String> startsWithN = new ArrayList<String>(); for(String name : friends) { if(name.startsWith(&

Java函数式编程(十一):遍历目录_java

列出目录中的文件 用File类的list()方法可以很容易的列出目录中的所有文件的文件名.如果想要获取文件而不止是文件名的话,可以使用它的listFiles()方法.这很简单,难的是怎么去处理这个返回的列表.我们不再使用传统的冗长的外部迭代器,而是使用优雅的函数式来实遍历这个列表.这里我们还得用到JDK的新的CloseableStream接口以及一些相关的高阶函数. 下面这段代码可以列出当前目录下所有文件的名字. 复制代码 代码如下: Files.list(Paths.get(".")