为什么需要流式操作

集合API是Java API中最重要的部分。基本上每一个java程序都离不开集合。尽管很重要,但是现有的集合处理在很多方面都无法满足需要。

一个原因是,许多其他的语言或者类库以声明的方式来处理特定的数据模型,比如SQL语言,你可以从表中查询,按条件过滤数据,并且以某种形式将数据分组,而不必需要了解查询是如何实现的——数据库帮你做所有的脏活。这样做的好处是你的代码很简洁。很遗憾,Java没有这种好东西,你需要用控制流程自己实现所有数据查询的底层的细节。

其次是你如何有效地处理包含大量数据的集合。理想情况下,为了加快处理过程,你会利用多核架构。但是并发程序不太好写,而且很容易出错。

Stream API很好的解决了这两个问题。它抽象出一种叫做流的东西让你以声明的方式处理数据,更重要的是,它还实现了多线程:帮你处理底层诸如线程、锁、条件变量、易变变量等等。

例如,假定你需要过滤出一沓发票找出哪些跟特定消费者相关的,以金额大小排列,再取出这些发票的ID。如果用Stream API,你很容易写出下面这种优雅的查询:


List ids
= invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.sorted(comparingDouble(Invoice::getAmount))
.map(Invoice::getId)
.collect(Collectors.toList());

本章后面,你将了解到这些代码流程的细节。

什么是流

说了这么多,到底什么是流?通俗地讲,你可以认为是支持类似数据库操作的“花哨的迭代器”。技术上讲,它是从某个数据源获得的支持聚合操作的元素序列。下面着重介绍一下正式的定义:
元素序列
针对特定元素类型的有序集合流提供了一个接口。但是流不会存储元素,只会根据要求对其做计算。
数据源
流所用到的数据源来自集合、数组或者I/O。
聚合操作
流支持类似数据库的操作以及函数式语言的基本操作,比如filter,map,reduce,findFirst,allMatch,sorted等待。

此外,流操作还有两种额外的基础属性根据不同的集合区分:
管道连接
许多流操作返回流本身,这种操作可以串联成很长的管道,这种方式更加有利于像延迟加载,短路,循环合并等操作。
内部迭代器
不像集合依赖外部迭代器,流操作在内部帮你实现了迭代器。

流操作

流接口在java.util.stream.Stream定义了许多操作,这些可以分为以下两类:

  • 像filter,sorted和map一样的可以被连接起来形成一个管道的操作。
  • 像collect,findFirst和allMatch一样的终止管道并返回数据的操作。

可以被连接起来的操作被称为中间操作,它们能被连接起来是因为都返回流。中间操作都“很懒”并且可以被优化。终止一个流管道的操作被叫做结束操作,它们从流管道返回像List,Integer或者甚至是void等非流类型的数据。
下面我们介绍一下流里面的一些方法,完整的方法列表可以在java.util.stream.Stream找到。

Filter

有好几个方法可以用来从流里面过滤出元素:
filter
通过传递一个预期匹配的对象作为参数并返回一个包含所有匹配到的对象的流。
distinct
返回包含唯一元素的流(唯一性取决于元素相等的实现方式)。
limit
返回一个特定上限的流。
skip
返回一个丢弃前n个元素的流。

List expensiveInvoices
= invoices.stream()
.filter(inv -> inv.getAmount() > 10_000)
.limit(5)
.collect(Collectors.toList());

Matching

匹配是一个判断是否匹配到给定属性的普遍的数据处理模式。你可以用anyMatch,allMatch和noneMatch来匹配数据,它们都需要一个预期匹配的对象作为参数并返回一个boolen型的数据。例如,你可以用allMatch来检查是否所有的发票流里面的元素的值都大于1000:

boolean expensive =
invoices.stream()
.allMatch(inv -> inv.getAmount() > 1_000);

Finding

此外,流接口还提供了像findFirst和findAny等从流中取出任意的元素。它们能与像filter方法相连接。findFirst和findAny都返回一个可选对象(我们已经在第一章中讨论过)。

Optional =
invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.findAny();

Mapping

流支持映射方法,传递一个函数对象作为方法,把流中的元素转换成另一种类型。这种方法应用于单个元素,将其映射成新元素。
例如,你有可能想用它来提取流中每个元素的信息。下面这段代码从一列发票中返回一列ID:

List ids
= invoices.stream()
.map(Invoice::getId)
.collect(Collectors.toList());

Reducing

另一个常用的模式是把数据源中的所有元素结合起来提供单一的值。例如,“计算最高金额的发票” 或者 “计算所有发票的总额”。 这可以应用流中的reduce方法反复应用于每个元素直到返回最后数据。
下面是reduce模式的例子,能帮你了解如何用for循环来计算一列数据的和:

int sum = 0;
for (int x : numbers) {
sum += x;
}

对一列数据的每一个元素的值反复应用加法运算符获得结果,最终将一列值减少到一个值。这段代码用到两个参数:初始化总和变量,这里是0;用来结合所有列表里面元素的操作方法,这里是加法操作。
在流上应用reduce方法,可以把流里面的所有元素相加,如下:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce方法需要两个参数:

  • 初始值,这里是0
  • 一个BinaryOperator方法连接两个元素产生一个新元素。reduce方法本质上是抽象了重复方法模式。其他查询像“计算总和” 或者“计算最大值” 都是reduce方法的特殊用例,比如:


int product = numbers.stream().reduce(1, (a, b) -> a * b);
int max = numbers.stream().reduce(Integer.MIN_VALUE,
Integer::max);

Collectors

目前为止你所了解的方法都是返回另一个流或者一个像boolean,int类型的值,或者返回一个可选对象。相比之下,collect方法是一个结束操作,它可以使流里面的所有元素聚集到汇总结果。
传递给collect方法参数是一个java.util.stream.Collector类型的对象。Collector对象实际上定义了一个如何把流中的元素聚集到最终结果的方法。最开始,工厂方法Collectors.toList()被用来返回一个描述了如何把流转变成一个List的Collector对象。后来Collectors类又内建了很多相似的collectors变量。例如,你可以用Collectors.groupingBy方法按消费者把发票分组,如下:

Map<Customer, List> customerToInvoices
= invoices.stream().collect(Collectors.group
ingBy(Invoice::getCustomer));

Putting It All Together

下面是一个手把手的例子你可以练习如何把老式代码用Stream API重构。下面代码的用途是按照特定消费者过滤出的与训练有关的发票,以金额高低排序,最后提取出最高的前5张发票的ID:

List oracleAndTrainingInvoices = new ArrayList();
List ids = new ArrayList();
List firstFiveIds = new ArrayList();
for(Invoice inv: invoices) {
if(inv.getCustomer() == Customer.ORACLE) {
if(inv.getTitle().contains("Training")) {
oracleAndTrainingInvoices.add(inv);
}
}
}
Collections.sort(oracleAndTrainingInvoices,
new Comparator() {
@Override
public int compare(Invoice inv1, Invoice inv2) {
return Double.compare(inv1.getAmount(), inv2.getA
mount());
}
});
for(Invoice inv: oracleAndTrainingInvoices) {
ids.add(inv.getId());
}
for(int i = 0; i < 5; i++) {
firstFiveIds.add(ids.get(i));
}

接下来,你将用Stream API一步一步地重构这些代码。首先,你或者注意到代码中用到了一个中间容器来存储那些消费者是Customer.ORACLE并且title中含有“Training”字段的发票。这正是应用filter方法的地方:

Stream oracleAndTrainingInvoices
= invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.filter(inv ->
inv.getTitle().contains("Training"));

接下来,你需要按照数量来把这些发票排序,你可以用新的工具方法Comparator.comparing结合sorted方法来实现:

Stream sortedInvoices
= oracleAndTrainingInvoices.sorted(comparingDou
ble(Invoice::getAmount));

下面,你需要提取ID,这是map方法的用途:

Stream ids
= sortedInvoices.map(Invoice::getId);

最后,你只对前5张发票感兴趣。你可以用limit方法截取这5张发票。当你整理一下代码,再用collect方法,最终的代码如下:

List firstFiveIds
= invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.filter(inv ->
inv.getTitle().contains("Training"))
.sorted(comparingDouble(Invoice::getAmount))
.map(Invoice::getId)
.limit(5)
.collect(Collectors.toList());

当你观察一下老式的代码你会发现每一个本地变量只被存储了一次,被下一段代码用了一次。当用Stream API之后,就完全消除了这个本地变量。

Parallel Streams

Stream API 支持方便的数据并行。换句话说,你可以明确地让流管道以并行的方式运行而不用关心底层的具体实现。在这背后,Stream API使用了Fork/Join框架充分利用了你机器的多核架构。
你所需要做的无非是用parallelStream()方法替换stream()方法。例如,下面代码显示如何并行地过滤金额高的发票:

List expensiveInvoices
= invoices.parallelStream()
.filter(inv -> inv.getAmount() > 10_000)
.collect(Collectors.toList());

此外,你可以用并行方法将现有的Stream转换成parallel Stream:

Stream expensiveInvoices
= invoices.stream()
.filter(inv -> inv.getAmount() > 10_000);
List result
= expensiveInvoices.parallel()
.collect(Collectors.toList());

然而,并不是所有的地方都可以用parallel Stream,从性能角度考虑,有几点你需要注意:
Splittability
parallel streams的内部实现依赖于将数据结构划分成可以让不同线程使用的难易程度。像数组这种数据结构很容易划分,而像链表或者文件这种数据结构很难划分。
Cost per element
越是计算流中单个元素花费的资源最高,应用并行越有意义。
Boxing
如果可能的话尽量用原始数据类型,这样可以占用更少的内存,也更缓存命中率也更高。
Size
流中元素的数据量越大越好,因为并行的成本会分摊到所有元素,并行节省的时间相对会更多。当然,这也跟单个元素计算的成本相关。
Number of cores
一般来说,核越多越好。
在实践中,如果你想提高代码的性能,你应该检测你代码的指标。Java Microbenchmark Harness (JMH) 是一个Oracle维护的流行的框架,你可以用它来帮你完成代码分析检测。如果不检测的话,简单的应用并行,代码的性能或许更差。

Summary

下面是本章的重点内容:

    • 流是一列支持聚合操作的来自于不同数据源的元素列表
    • 流有两种类型的操作方法:中间方法和终结方法
    • 中间方法可以被连接起来形成管道
    • 中间方法包括filter,map,distinct和sorted
    • 终结方法处理流管道并返回一个结果
    • 终结方法包括allMatch,collect和forEach
    • Collectors是一个第应以了如何将流中的元素聚集到最终结果的方法,包括像List和Map一样的容器
    • 流管道可以被并行地计算
    • 当应用parallel stream 来提高性能时有很多个方面需要考虑,包括数据结构划分的难易程度,计算每个元素花费的高低,装箱的难易,数据量的多少和可用核的数量。

java8 流式编程的更多相关文章

  1. java8流式编程(一)

    传送门 <JAVA8开发指南>为什么你需要关注 JAVA8 <Java8开发指南>翻译邀请 Java8初体验(一)lambda表达式语法 Java8初体验(二)Stream语法 ...

  2. 20190827 On Java8 第十四章 流式编程

    第十四章 流式编程 流的一个核心好处是,它使得程序更加短小并且更易理解.当 Lambda 表达式和方法引用(method references)和流一起使用的时候会让人感觉自成一体.流使得 Java ...

  3. golang的极简流式编程实现

    传统的过程编码方式带来的弊端是显而易见,我们经常有这样的经验,一段时间不维护的代码或者别人的代码,突然拉回来看需要花费较长的时间,理解原来的思路,如果此时有个文档或者注释写的很好的话,可能花的时间会短 ...

  4. Stream流式编程

    Stream流式编程   Stream流 说到Stream便容易想到I/O Stream,而实际上,谁规定“流”就一定是“IO流”呢?在Java 8中,得益于Lambda所带来的函数式编程,引入了一个 ...

  5. JDK8新特性(二) 流式编程Stream

    流式编程是1.8中的新特性,基于常用的四种函数式接口以及Lambda表达式对集合类数据进行类似流水线一般的操作 流式编程分为大概三个步骤:获取流 → 操作流 → 返回操作结果 流的获取方式 这里先了解 ...

  6. Spark流式编程介绍 - 编程模型

    来源Spark官方文档 http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#programm ...

  7. Java8 流式 API(`java.util.stream`)

    熟悉 ES6 的开发者,肯定对数组的一些方法不是很陌生:map.filter 等.在对一组对象进行统一操作时,利用这些方法写出来的代码比常规的迭代代码更加的简练.在 C♯ 中,有 LINQ 来实现.那 ...

  8. Java8 新特性 —— Stream 流式编程

    本文部分摘自 On Java 8 流概述 集合优化了对象的存储,大多数情况下,我们将对象存储在集合是为了处理他们.使用流可以帮助我们处理对象,无需迭代集合中的元素,即可直接提取和操作元素,并添加了很多 ...

  9. java8 stream api流式编程

    java8自带常用的函数式接口 Predicate boolean test(T t) 传入一个参数返回boolean值 Consumer void accept(T t) 传入一个参数,无返回值 F ...

随机推荐

  1. Make编译Ardupilot源码的两种方法

    编译环境准备 ​ Ardupilot源码下载和PX4 toolchain工具链下载 ​ (见https://www.cnblogs.com/BlogsOfLei/p/7707485.html) ​ 注 ...

  2. 模块 configparser 配置文件生成修改

    此模块用于生成和修改常见配置文档 1.来看一个好多软件的常见配置文件格式如下***.ini [DEFAULT] ServerAliveInterval = 45 Compression = yes C ...

  3. 20175314 《Java程序设计》第十周学习总结

    20175314 <Java程序设计>第十周学习总结 教材学习内容总结 进程与线程:一个进程的进行期间可以产生多个线程. Java内置对多线程的支持,计算机只能执行线程中的一个,Java虚 ...

  4. 线程间交换数据的Exchanger

    作者:Steven1997 链接:https://www.jianshu.com/p/9b59829fb191 来源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处. Exc ...

  5. A换算时间(只想开学)HDU 6556

    题目链接 思路如下 把时间转化为 24小时制下进行考虑,首先我们要明白(在24小时制下):12 点表示是下午PM ,而 24点表示的是明天的 0点(12小时制下),这两个地方需要特殊考虑 题解如下 # ...

  6. apache-atlas 深度剖析

    atlas  是apache下的大数据的元数据管理平台,支持对hive.storm.kafka.hbase.sqoop等进行元数据管理以及以图库的形式展示数据的血缘关系. 一.架构 整体架构实现如下图 ...

  7. stm32:简单按键输入实现

    开发环境keil4,芯片STM32F103C8T6 1.main.c //串口实验 #include "sys.h" #include "delay.h" #i ...

  8. Java8 学习笔记--函数式接口与lambda表达式的关系

    在java中,lambda表达式与函数式接口是不可分割的,都是结合起来使用的. 对于函数式接口,我们可以理解为只有一个抽象方法的接口,除此之外它和别的接口相比并没有什么特殊的地方.为了确保函数式接口的 ...

  9. .NET Core项目部署到Linux(Centos7)(八)为.NET Core项目创建Supervisor进程守护监控

    目录 1.前言 2.环境和软件的准备 3.创建.NET Core API项目 4.VMware Workstation虚拟机及Centos 7安装 5.Centos 7安装.NET Core环境 6. ...

  10. 怎么用scratch做大鱼吃小鱼

    行走代码不说了.出鱼代码大概就是 棋子被点击时 重复执行 移到x:从()到()任意选一个数,y一样 克隆自己 等待你想要的秒数.吃鱼代码就是 当作为克隆体启动是 重复执行 如果碰到()那么 删除克隆体 ...