《JAVA8开发指南》使用流式操作

本章中,你将学习到怎样使用Stream API进行开发。首先,你将会了解Stream API背后的机制,什么是流以及流的用处。其次,你将学习到一系列的流式操作、流式数据处理模型以及能让你写出更复杂数据查询的流式集合操作。接下来是如何应用流式操作的例子。最后,你将学习到并行流。

为什么需要流式操作

集合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 来提高性能时有很多个方面需要考虑,包括数据结构划分的难易程度,计算每个元素花费的高低,装箱的难易,数据量的多少和可用核的数量。
  • 转载自 并发编程网 - ifeve.com
时间: 2024-11-17 18:33:21

《JAVA8开发指南》使用流式操作的相关文章

《JAVA8开发指南》为什么你需要关注 JAVA8

本章包含 代码的可读性 多核 JAVA8特性的快速指南 JAVA8:为什么你需要关注? JAVA已经更新了!在 2014 年 3 月,JAVA发布了新版本-JAVA8,JAVA8 引入的一些新特性可能会改变你日常中基本的编码方式.但不用担心,这本简洁的指南会带着你掌握一些要领,现在你就可以开始阅读. 第一章列举了 JAVA8 中增加的主要功能概况.接下来的两章则关注 JAVA8 的主要特性: lambda 表达式 和streams(流). 驱动 JAVA8 改进的两大动机: 代码可读性 更加简化

《JAVA8开发指南》第二章采用Lambda表达式(二)

注意事项 你会经常看到在接口类上标有@FunctionalInterface的注解.它和标签@Override很相似,表示方法是可以被覆盖的.本文中,@FunctionalInterface标签用来写在文档中,表示接口是一个函数接口.编译器也将在接口注解不符合函数接口定义的时候抛错. 你也将发现一些新的函数接口,比如在包java.util.function里面的Function<T, R>和Supplier<T>,你可以通过这些来使用任意形式的lambda表达式. 方法参数 方法参

《Java8开发指南》翻译邀请

Java 8: Why Should You Care? Adopting Lambda Expressions. Adopting Streams 转载自 并发编程网 - ifeve.com

Simple JSON开发指南_java

Simple JSON是Google开发的Java JSON解析框架,基于Apache协议. json-simple的主页:http://www.jb51.net/softs/455885.html 下载的文件是:json_simple.jar 例子1:很方便的方式,使用JSONValue System.out.println("=======decode======="); String s="[0,{\"1\":{\"2\":{\&

Struts开发指南之MVC架构

模型-视图-控制器(MVC)是80年代Smalltalk-80出现的一种软件设计模式,现在已经被广泛的使用. 1.模型(Model) 模型是应用程序的主体部分.模型表示业务数据,或者业务逻辑. 2.视图(View) 视图是应用程序中用户界面相关的部分,是用户看到并与之交互的界面. 3.控制器(controller) 控制器工作就是根据用户的输入,控制用户界面数据显示和更新model对象状态. 开发指南之MVC架构-springmvc开发指南pdf"> MVC 式的出现不仅实现了功能模块和显

《Access 2007开发指南(修订版)》一一2.5 字段属性操作

2.5 字段属性操作 Access 2007开发指南(修订版) 在表中添加了字段之后,就应该自定义它们的属性.可以使用字段属性来控制数据的存储方式,以及什么样的数据可以输入到字段当中.根据所选定字段类型的不同,字段的属性也会有所不同."文本"字段类型的字段属性列表具有一定的代表性(如图2.8所示).以下各节就专门讨论各个字段属性. 2.5.1 字段大小:限制对字段的输入 第1个属性是字段大小,它只适用于文本和数字字段.如前所述,应该使字段大小尽可能小.对数字字段来说,字段越小,对存储空

《Access 2007开发指南(修订版)》一一2.8 表的属性操作

2.8 表的属性操作 Access 2007开发指南(修订版)除了字段属性之外,还可以总体地为表指定属性.为了访问表的属性,应该在表的设计视图中单击功能区上的"属性表"按钮.图2.22所示的是表的属性."说明"属性主要用于编写文档."默认视图"属性在第一次打开表时,指定表出现在哪个视图中."有效性规则"属性用于指定记录级而不是字段级的有效性验证.例如,对于不同州的顾客来说,其CreditLimit应该不一样.在这种情况下,一个

node.js 开发指南 – Node.js 连接 MySQL 并进行数据库操作_node.js

Node.js是一套用来编写高性能网络服务器的JavaScript工具包   通常在NodeJS开发中我们经常涉及到操作数据库,尤其是 MySQL ,作为应用最为广泛的开源数据库则成为我们的首选,本篇就来介绍下如何通过NodeJS来操作 MySQL 数据库. 安装MySQL模块到NodeJS中 我们需要让NodeJS支持MySQL,则需要将MySQL模块添加到系统支持库   想要快速了解Node.js ,赞生推荐亲看看 node.js_guide.pdf  - node.js 开发指南 :想要电

Java8简明指南

Java8简明指南 欢迎来到Java8简明指南.本教程将一步一步指导你通过所有新语言特性.由短而简单的代码示例,带你了解如何使用默认接口方法,lambda表达式,方法引用和可重复注解.本文的最后你会熟悉最新的API的变化如Stream,Fcuntional,Map API扩展和新的日期API.   接口的默认方法 在Java8中,利用default关键字使我们能够添加非抽象方法实现的接口.此功能也被称为扩展方法,这里是我们的第一个例子: interface Formula { double ca