您好、欢迎来到现金彩票网!
当前位置:秒速时时彩计划 > 随机归约 >

Java编程的逻辑 (92) - 函数式数据处理 (上)

发布时间:2019-06-07 06:35 来源:未知 编辑:admin

  本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接

  上节我们介绍了Lambda表达式和函数式接口,本节探讨它们的应用,函数式数据处理,针对常见的集合数据处理,Java 8引入了一套新的类库,位于包java.util.stream下,称之为Stream API,这套API操作数据的思路,不同于我们在38节55节介绍的容器类API,它们是函数式的,非常简洁、灵活、易读,具体有什么不同呢?由于内容较多,我们分为两节来介绍,本节先介绍一些基本的API,下节讨论一些高级功能。

  接口Stream类似于一个迭代器,但提供了更为丰富的操作,Stream API的主要操作就定义在该接口中。 Java 8给Collection接口增加了两个默认方法,它们可以返回一个Stream,如下所示:

  stream()返回的是一个顺序流,parallelStream()返回的是一个并行流。顺序流就是由一个线程执行操作。而并行流背后可能有多个线程并行执行,与之前介绍的并发技术不同,使用并行流不需要显式管理线程,使用方法与顺序流是一样的。

  下面,我们主要针对顺序流,学习Stream接口,包括其用法和基本原理,随后我们再介绍下并行流。先来看一些简单的示例。

  代码更为简洁易读了,这种数据处理方式被称为函数式数据处理,与传统代码相比,它的特点是:

  这里使用了Stream的map函数,它的参数是一个Function函数式接口,这里传递了方法引用。

  这种组合利用基本函数、声明式实现集合数据处理功能的编程风格,就是函数式数据处理。

  代码更为直观易读了,但你可能会担心它的性能有问题。filter()和map()都需要对流中的每个元素操作一次,一起使用会不会就需要遍历两次呢?答案是否定的,只需要一次。实际上,调用filter()和map()都不会执行任何实际的操作,它们只是在构建操作的流水线,调用collect才会触发实际的遍历执行,在一次遍历中完成过滤、转换以及收集结果的任务。

  像filter和map这种不实际触发执行、用于构建流水线、返回Stream的操作被称为中间操作(intermediate operation),而像collect这种触发实际执行、返回具体结果的操作被称为终端操作(terminal operation)。Stream API中还有更多的中间和终端操作,下面我们具体来看下。

  distinct返回一个新的Stream,过滤重复的元素,只留下唯一的元素,是否重复是根据equals方法来比较的,distinct可以与其他函数如filter, map结合使用。

  比如,返回字符串列表中长度小于3的字符串、转换为小写、只保留唯一的,代码可以为:

  虽然都是中间操作,但distinct与filter和map是不同的,filter和map都是无状态的,对于流中的每一个元素,它的处理都是独立的,处理后即交给流水线中的下一个操作,但distinct不同,它是有状态的,在处理过程中,它需要在内部记录之前出现过的元素,如果已经出现过,即重复元素,它就会过滤掉,不传递给流水线中的下一个操作。

  对于顺序流,内部实现时,distinct操作会使用HashSet记录出现过的元素,如果流是有顺序的,需要保留顺序,会使用LinkedHashSet。

  它们都对流中的元素排序,都返回一个排序后的Stream,第一个方法假定元素实现了Comparable接口,第二个方法接受一个自定义的Comparator。

  比如,过滤得到90分以上的学生,然后按分数从高到低排序,分数一样的,按名称排序,代码可以为:

  与distinct一样,sorted也是一个有状态的中间操作,在处理过程中,需要在内部记录出现过的元素,与distinct不同的是,每碰到流中的一个元素,distinct都能立即做出处理,要么过滤,要么马上传递给下一个操作,但sorted不能,它需要先排序,为了排序,它需要先在内部数组中保存碰到的每一个元素,到流结尾时,再对数组排序,然后再将排序后的元素逐个传递给流水线中的下一个操作。

  skip跳过流中的n个元素,如果流中元素不足n个,返回一个空流,limit限制流的长度为maxSize。

  skip和limit都是有状态的中间操作。对前n个元素,skip的操作就是过滤,对后面的元素,skip就是传递给流水线中的下一个操作。limit的一个特点是,它不需要处理流中的所有元素,只要处理的元素个数达到maxSize,后面的元素就不需要处理了,这种可以提前结束的操作被称为短路操作。

  它返回的流与之前的流是一样的,没有变化,但它提供了一个Consumer,会将流中的每一个元素传给该Consumer。这个方法的主要目的是支持调试,可以使用该方法观察在流水线中流转的元素,比如:

  map函数接受的参数是一个FunctionT, R,为避免装箱/拆箱,提高性能,Stream还有如下返回基本类型特定流的方法:

  DoubleStream/IntStream/LongStream是基本类型特定的流,有一些专门的更为高效的方法。比如,求学生列表的分数总和,代码可以为:

  它接受一个函数mapper,对流中的每一个元素,mapper会将该元素转换为一个流Stream,然后把新生成流的每一个元素传递给下一个操作。比如:

  这里的mapper将一行字符串按空白符分隔为了一个单词流,Arrays.stream可以将一个数组转换为一个流,输出为:

  它们返回流中的最大值/最小值,值的注意的是,它的返回值类型是OptionalT,而不是T。

  java.util.Optional是Java 8引入的一个新类,它是一个泛型容器类,内部只有一个类型为T的单一变量value,可能为null,也可能不为null。Optional有什么用呢?它用于准确地传递程序的语义,它清楚地表明,其代表的值可能为null,程序员应该进行适当的处理。

  在max/min的例子中,通过声明返回值为Optional,我们就知道,具体的返回值不一定存在,这发生在流中不含任何元素的情况下。

  count很简单,就是返回流中元素的个数。比如,统计大于90分的学生个数,代码可以为:

  这几个函数都接受一个谓词Predicate,返回一个boolean值,用于判定流中的元素是否满足一定的条件,它们的区别是:

  这几个操作都是短路操作,都不一定需要处理所有元素就能得出结果,比如,对于allMatch,只要有一个元素不满足条件,就能返回false。

  它们的返回类型都是Optional,如果流为空,返回Optional.empty()。findFirst返回第一个元素,而findAny返回任一元素,它们都是短路操作。

  它们都接受一个Consumer,对流中的每一个元素,传递元素给Consumer,区别在于,在并行流中,forEach不保证处理的顺序,而forEachOrdered会保证按照流中元素的出现顺序进行处理。

  不带参数的toArray返回的数组类型为Object[],这经常不是期望的结果,如果希望得到正确类型的数组,需要传递一个类型为IntFunction的generator,IntFunction的定义为:

  generator接受的参数是流的元素个数,它应该返回对应大小的正确类型的数组。

  reduce代表归约或者叫折叠,它是max/min/count的更为通用的函数,将流中的元素归约为一个值,有三个reduce函数:

  第二个reduce函数多了一个identity参数,表示初始值,它基本等同于调用:

  第一个和第二个reduce的返回类型只能是流中元素的类型,而第三个更为通用,它的归约类型可以自定义,另外,它多了一个combiner参数,combiner用在并行流中,用于合并子线程的结果,对于顺序流,它基本等同于调用:

  注意与第二个reduce函数相区分,它的结果类型不是T,而是U。比如,使用reduce函数计算学生分数的和,代码可以为:

  以上,可以看出,reduce虽然更为通用,但比较费解,难以使用,一般情况,应该优先使用其他函数。

  collect函数比reduce更为通用、强大和易用,关于它,我们下节再详细介绍。

  前面我们提到,可以通过Collection接口的stream/parallelStream获取流,还有一些其他的方式可以获取流。

  前面我们主要使用的是Collection的stream()方法,换做parallelStream()方法,就会使用并行流,接口方法都是通用的。但并行流内部会使用多线程,线程个数一般与系统的CPU核数一样,以充分利用CPU的计算能力。

  进一步来说,并行流内部会使用Java 7引入的fork/join框架,简单来说,处理由fork和join两个阶段组成,fork就是将要处理的数据拆分为小块,多线程按小块进行并行计算,join就是将小块的计算结果进行合并,具体我们就不探讨了。使用并行流,不需要任何线程管理的代码,就能实现并行。

  看的出来,使用Stream API处理数据集合,与直接使用容器类API处理数据的思路是完全不一样的。

  流定义了很多数据处理的基本函数,对于一个具体的数据处理问题,解决的主要思路就是组合利用这些基本函数,实现期望的功能,这种思路就是函数式数据处理思维,相比直接利用容器类API的命令式思维,思考的层次更高。

  Stream API的这种思路也不是新发明,它与数据库查询语言SQL是很像的,都是声明式地操作集合数据,很多函数都能在SQL中找到对应,比如filter对应SQL的where,sorted对应order by等。SQL一般都支持分组(group by)功能,Stream API也支持,但关于分组,我们下节再介绍。

  Stream API也与各种基于Unix系统的管道命令类似,熟悉Unix系统的都知道,Unix有很多命令,大部分命令只是专注于完成一件事情,但可以通过管道的方式将多个命令链接起来,完成一些复杂的功能,比如:

  以上命令可以分析nginx访问日志,统计出访问次数最多的前20个IP地址及其访问次数。具体来说,cat命令输出nginx访问日志到流,一行为一个元素,awk输出行的第一列,这里为IP地址,sort按IP进行排序,uniq -c按IP统计计数,sort -rnk 1按计数从高到低排序,head -n 20输出前20行。

  本节初步介绍了Java 8引入的函数式数据处理类库,Stream API,它类似于Unix的管道命令,也类似于数据库查询语言SQL,通过组合利用基本函数,可以在更高的层次上思考问题,以声明式的方式简洁地实现期望的功能。

  对于collect方法,本节只是演示了最基本的应用,它还有很多高级功能,比如实现类似SQL的group by功能,具体怎么实现?实现的原理是什么呢?

  (与其他章节一样,本节所有代码位于 ,位于包ma.java8.c92下)

http://parroche-dorioz.com/suijiguiyue/133.html
锟斤拷锟斤拷锟斤拷QQ微锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷微锟斤拷
关于我们|联系我们|版权声明|网站地图|
Copyright © 2002-2019 现金彩票 版权所有