169it科技资讯


当前位置:  编程技术>java/j2ee

Java函数式编程(七):MapReduce

    来源: 互联网  发布时间:2014-11-08

译注: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表达式如何简化程序开发中的另一个基本操作:字符串操作以及对象比较。


    
相关技术文章:
    ▪Java函数式编程(八):字符串及方法引用

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


    ▪Java函数式编程(九):Comparator

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


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

     前面我们已经用过几次collect()方法来将Stream返回的元素拼成ArrayList了。这是一个reduce操作,它对于将一个集合转化成另一种类型(通常是一个可变的集合)非常有用。collect()函数,如果和Collectors工具类里的一些方法结合起来使用的话,能提供极大的便利性,本节我们将会介绍到。 我们还是继续使用前面的Person列表作为例子,来看一下collect()方法到底有哪些能耐。假设我们要从原始列表中找出所有大于20岁的人。下面是使用了可变性和forEach()方法实现的版本: 代码如下: List<Person> olderThan20 = new......


 
最新技术文章:
    ▪Java中使用开源库JSoup解析HTML文件实例

     HTML是WEB的核心,互联网中你看到的所有页面都是HTML,不管它们是由JavaScript,JSP,PHP,ASP或者是别的什么WEB技术动态生成的。你的浏览器会去解析HTML并替你去渲染它们。不过如果你需要自己在Java程序中解析HTML文档并查找某些元素,标签,属性或者检查某个特定的元素是否存在的话,那又该如何呢?如果你已经使用Java编程多年了,我相信你肯定试过去解析XML,也使用过类似DOM或者SAX这样的解析器,不过很有可能你从未进行过任何的HTML解析的工作。更讽刺的是,在Java应用中,很少会有需要你去解析HTML文档的时候,这里并不包括Servlet或者其它的Java WEB技术。更糟糕......


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

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


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

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


 


站内导航:


特别声明:169IT网站部分信息来自互联网,如果侵犯您的权利,请及时告知,本站将立即删除!

©2012-2017,169IT.COM,E-mail:www_169it_com#163.com(请将#改为@)

浙ICP备11055608号