集合API是Java API中最重要的部分。基本上每个java程序都离不开集合。尽管很重要,可是现有的集合处理在不少方面都没法知足须要。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定义了许多操做,这些能够分为如下两类:oracle
能够被链接起来的操做被称为中间操做,它们能被链接起来是由于都返回流。中间操做都“很懒”而且能够被优化。终止一个流管道的操做被叫作结束操做,它们从流管道返回像List,Integer或者甚至是void等非流类型的数据。
下面咱们介绍一下流里面的一些方法,完整的方法列表能够在java.util.stream.Stream找到。app
有好几个方法能够用来从流里面过滤出元素:
filter
经过传递一个预期匹配的对象做为参数并返回一个包含全部匹配到的对象的流。
distinct
返回包含惟一元素的流(惟一性取决于元素相等的实现方式)。
limit
返回一个特定上限的流。
skip
返回一个丢弃前n个元素的流。
List expensiveInvoices
= invoices.stream()
.filter(inv -> inv.getAmount() > 10_000)
.limit(5)
.collect(Collectors.toList());
匹配是一个判断是否匹配到给定属性的广泛的数据处理模式。你能够用anyMatch,allMatch和noneMatch来匹配数据,它们都须要一个预期匹配的对象做为参数并返回一个boolen型的数据。例如,你能够用allMatch来检查是否全部的发票流里面的元素的值都大于1000:
boolean expensive =
invoices.stream()
.allMatch(inv -> inv.getAmount() > 1_000);
此外,流接口还提供了像findFirst和findAny等从流中取出任意的元素。它们能与像filter方法相链接。findFirst和findAny都返回一个可选对象(咱们已经在第一章中讨论过)。
Optional =
invoices.stream()
.filter(inv ->
inv.getCustomer() == Customer.ORACLE)
.findAny();
流支持映射方法,传递一个函数对象做为方法,把流中的元素转换成另外一种类型。这种方法应用于单个元素,将其映射成新元素。
例如,你有可能想用它来提取流中每一个元素的信息。下面这段代码从一列发票中返回一列ID:
List ids
= invoices.stream()
.map(Invoice::getId)
.collect(Collectors.toList());
另外一个经常使用的模式是把数据源中的全部元素结合起来提供单一的值。例如,“计算最高金额的发票” 或者 “计算全部发票的总额”。 这能够应用流中的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方法须要两个参数:
int product = numbers.stream().reduce(1, (a, b) -> a * b);
int max = numbers.stream().reduce(Integer.MIN_VALUE,
Integer::max);
目前为止你所了解的方法都是返回另外一个流或者一个像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));
下面是一个手把手的例子你能够练习如何把老式代码用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以后,就彻底消除了这个本地变量。
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维护的流行的框架,你能够用它来帮你完成代码分析检测。若是不检测的话,简单的应用并行,代码的性能或许更差。
下面是本章的重点内容: