函数式数据处理

1.流概述
流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。可以把它们看成遍历数据集的高级迭代器。流还可以透明地并行处理,无需写任何多线程代码。
Java 8中的Stream API可以让你写出这样的代码:
◆声明性——更简洁,更易读
◆可复合——更灵活
◆可并行——性能更好
Java 8中的集合支持一个新的stream方法,它会返回一个流。
1.1 流的概念
简短的定义就是“从支持数据处理操作的源生成的元素序列”。
◆元素序列——像集合一样,流也提供一个接口,可以访问特定元素类型的一组有序值;
◆源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致;
◆数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、 map、 reduce、 find、 match、 sort等。流操作可顺序执行,也可并行执行。
特点:
◆流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询;
◆内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
1.2 流的特点
◆流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的;
◆和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了;
◆使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。 相反,Streams库使用内部迭代——它把迭代做了,把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。
1.3 流操作
可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
A.中间操作
中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理,因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
B.终端操作
终端操作从流的流水线生成结果。结果是任何不是流的值,比如List、 Integer,甚至void。
C.流的使用
流的使用一般包括三件事:
◆一个数据源(如集合)来执行一个查询;
◆一个中间操作链,形成一条流的流水线;
◆一个终端操作,执行流水线,并能生成结果。
流的流水线背后的理念类似于构建器模式。 ①在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用built方法(对流来说就是终端操作)。

2.流操作
2.1 筛选和切片
用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。
◆Streams接口支持filter方法,该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。
◆distinct:返回一个元素不重复(根据流所生成元素的hashCode和equals方法实现)的流;
◆limit(n):返回一个不超过给定长度的流;
◆skip(n):返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流;
2.2. 映射:从某些对象中选择信息
◆map:接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(它和转换类似,差别在于它是“创建一个新版本”而不是去“修改”)。
◆flatMap:把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流;
2.3. 查找和匹配:查找数据集中的某些元素是否匹配一个给定的属性
◆anyMatch:返回一个boolean,查找流中是否有一个元素能匹配给定的谓词;
◆allMatch:查找流中的元素是否都能匹配给定的谓词;
◆noneMatch:确保流中没有任何元素与给定的谓词匹配;
◆findAny:返回当前流中的任意元素。它可以与其他流操作结合使用;
◆findFirst:查找到第一个元素;
2.4.归约
reduce:把一个流中的元素组合起来

2.5.数值流
原始类型特化流接口来解决这个问题: IntStream、 DoubleStream和LongStream,分别将流中的元素特化为int、 long和double,从而避免了暗含的装箱成本。
A.映射到数值流
将流转换为特化版本的常用方法是mapToInt、 mapToDouble和mapToLong;
B. 转换回对象流
boxed:把原始流转换成一般流;
C. 默认值
OptionalInt:默认值;
D.数值范围
range和rangeClosed:生成数值范围,range不包含结束值,而rangeClosed则包含结束值;
2.6.构建流
◆Stream.of:由值创建流;
◆Arrays.stream:由数组创建流;
◆Files.lines:由指定文件中的各行构成的字符串流;
◆Stream.iterate和Stream.generate:可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
3.用流收集数据
3.1 收集器
Collector接口,收集器可以简洁而灵活地定义collect用来生成结果集合的标准。更具体地说,对流调用collect方法将对流中的元素触发一个归约操作(由Collector来参数化)。一般来说,Collector会对元素应用一个转换函数,并将结果累积在一个数据结构中,从而产生这一过程的最终输出。
3.2 预定义收集器
A.将流元素归约和汇总为一个值
◆counting:获取总数;
◆maxBy,minBy:获取最大最小值;
◆summingInt,summingLong和summingDouble:接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作;
◆averagingInt,averagingLong和averagingDouble:计算数值的平均数;
◆summarizing,LongSummaryStatistics和DoubleSummaryStatistics:获取元素的个数,总和、平均值、最大值和最小值;
◆joining:把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串;
◆reducing:Collectors.reducing工厂方法是上术情况的一般化;
B.元素分组
◆groupingBy:分组
C.元素分区
分区是分组的特殊情况,分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以分为两组——true是一组, false是一组。
◆partitioningBy:分区

3.3 收集器接口
A.Collector接口
public interface Collector<T, A, R> {
//**建立新的结果容器
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
◆T是流中要收集的项目的泛型。
◆A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
◆R是收集操作得到的对象(通常但并不一定是集合)的类型。
B.说明
◆建立新的结果容器: supplier方法
supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。
◆将元素添加到结果容器: accumulator方法
accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n-1 个项目), 还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。
◆对结果容器应用最终转换: finisher方法
在遍历完流后, finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。
◆合并两个结果容器: combiner方法
combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。对流进行并行归约会用到Java 7中引入的分支/合并框架和Spliterator抽象。
◆characteristics方法
characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。
Characteristics是一个包含三个项目的枚举:
UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。
CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。
IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。累加器A将不加检查地转换为结果R。
C.进行自定义收集而不去实现Collector
对于IDENTITY_FINISH的收集操作,Stream有一个重载的collect方法可以接受另外三个函数—supplier、accumulator和combiner,其语义和Collector接口的相应方法返回的函数相同。
4.并行数据处理与性能
4.1 并行流
可以通过对收集源调用parallelStream方法来把集合转换为并行流。 并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
parallel/sequential:并行流/顺序流转换;
A.使用并行流需要考虑的问题
◆如果有疑问,测量;
◆留意装箱;
◆有些操作本身在并行流上的性能就比顺序流差;
◆要考虑流的操作流水线的总计算成本;
◆对于较小的数据量,选择并行流几乎从来都不是一个好的决定;
◆要考虑流背后的数据结构是否易于分解;
◆流自身的特点,以及流水线中的中间操作修改流的方式,都可能会改变分解过程的性能;
◆还要考虑终端操作中合并步骤的代价是大是小;

4.2 分支/合并框架
分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任务的结果合并起来生成整体结果。它是ExecutorService接口的一个实现,它把子任务分配给线程池(称为ForkJoinPool)中的工作线程。
A.使用分支/合并框架的最佳做法
◆对一个任务调用join方法会阻塞调用方,直到该任务做出结果。因此,有必要在两个子任务的计算都开始之后再调用它;
◆不应该在RecursiveTask内部使用ForkJoinPool的invoke方法。相反,应该始终直接调用compute或fork方法,只有顺序代码才应该用invoke来启动并行计算;
◆对子任务调用fork方法可以把它排进ForkJoinPool。同时对左边和右边的子任务调用fork的效率要比直接对其中一个调用compute低;
◆调试使用分支/合并框架的并行计算可能有点棘手;
◆和并行流一样,在多核处理器上使用分支/合并框架不一定比顺序计算快。
B.工作窃取(work stealing)
分支/合并框架工程用一种称为工作窃取(work stealing)的技术,在实际应用中,这意味着这些任务差不多被平均分配到ForkJoinPool中的所有线程上。一般来说,这种工作窃取算法用于在池中的工作线程之间重新分配和平衡任务。
4.3 Spliterator
Spliterator是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitable iterator)。和Iterator一样, Spliterator也用于遍历数据源中的元素,但它是为了并行执行而设计的。
Spliterator还有一个值得注意的功能,就是可以在第一次遍历、第一次拆分或第一次查询估计大小时绑定元素的数据源,而不是在创建时就绑定。这种情况下,它称为延迟绑定(late-binding)的Spliterator。
public interface Spliterator<T> {
//**顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍历就返回true
boolean tryAdvance(Consumer<? super T> action);
//**可以把一些元素划出去分给第二个Spliterator,让它们两个并行处理
Spliterator<T> trySplit();
//**估计还剩下多少元素要遍历
long estimateSize();
//**代表Spliterator本身特性集的编码,可以用这些特性来更好地控制和优化它的使用
int characteristics();
}

A.拆分过程
将Stream拆分成多个部分的算法是一个递归过程,
◆对第一个Spliterator调用trySplit,生成第二个Spliterator;
◆对这两个Spliterator调用trysplit,这样总共就有了四个Spliterator;
◆不断对Spliterator调用trySplit直到它返回null,表明它处理的数据结构不能再分割。
5.代码




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值