Java 的集合 API 中,仅仅有极少量的辅助型方法,更多的时候是程序员需要用 Iterator 来遍历集合,完成相关的聚合应用逻辑。这是一种远不够高效、笨拙的方法。
Streams 简介
为什么需要 Stream
Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。
什么是聚合操作
在传统的 J2EE 应用中,Java 代码经常不得不依赖于关系型数据库的聚合操作来完成诸如:
- 客户每月平均消费金额
- 最昂贵的在售商品
- 本周完成的有效订单(排除了无效的)
- 取十个数据样本作为首页推荐
这类的操作。
举个示例代码来说明,在 Java 7 中,如果要发现 type 为 grocery 的所有交易,然后返回以交易值降序排序好的交易 ID 集合,我们需要这样写:
排序、取值实现:
// Java 7
List<Transaction> groceryTransactions = new Arraylist<>();
// 第一步:For循环筛选出满足条件的对象添加至集合:groceryTransactions
for(Transaction t: transactions){
if(t.getType() == Transaction.GROCERY){
groceryTransactions.add(t);
}
}
// 第二步:将集合:groceryTransactions 执行排序
Collections.sort(groceryTransactions, new Comparator(){
public int compare(Transaction t1, Transaction t2){
return t2.getValue().compareTo(t1.getValue());
}
});
List<Integer> transactionIds = new ArrayList<>();
// 第三步:将排序好的列表通过For循环将所以的ID添加到新的集合:transactionIds
for(Transaction t: groceryTransactions){
transactionsIds.add(t.getId());
}
而在 Java 8 使用 Stream,代码如下:
// Java 8
List<Integer> transactionsIds = transactions.parallelStream()
.filter(t -> t.getType() == Transaction.GROCERY) // 筛选
.sorted(comparing(Transaction::getValue).reversed()) // 排序
.map(Transaction::getId) // 集合转换:返回一个列表,其中包含将给定转换函数应用于原始集合中的每个元素的结果
.collect(toList()); // Stream to List
是不是感觉 代码更加简洁易读;而且Java 8 的 Stream 使用并发模式,程序执行速度更快。总之 很🐂 !!!
那什么是流呢?
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。
当我们使用一个流的时候,通常包括三个基本步骤:
获取一个数据源(source) → 数据转换 → 执行操作获取想要的结果,
简单的说:每次转换,原有 Stream 对象不改变, 然后通过一系列 流 的操作,返回一个新的 Stream 对象(过程可以有多次转换)。
那怎么去生成 Stream Source?
有多种方式生成 Stream Source:
从 Collection 和数组
- Collection.stream()
- Collection.parallelStream()
- Arrays.stream(T array) or Stream.of()
从 BufferedReader
- java.io.BufferedReader.lines()
静态工厂
java.util.stream.IntStream.range()
java.nio.file.Files.walk()
自己构建
- java.util.Spliterator
其它
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
流的操作类型有哪些?
分为两种:
- Intermediate :一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
- Terminal :一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
对于以上的说明,再阐述一下,希望可以更容易的理解,这里我们假设:Intermediate类型的操作有 A(), B(), C(), D(), E() 。 Terminal类型的操作有 a(), b() 。 此时我们可以执行A,B,C,D,E操作中的一个或多个,但如果需要执行a,b操作,只能执行其中一个且必须放在最后执行。
示例可以简单写下:
Collection.stream().A().C().D().E().b()
流的操作时间复杂度:
在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都完成的总和吗?
- 其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。
还是 以上述的示例 来说明:
它经历了5次的流操作:A,C,D,E,b; 该示例的时间复杂度 并不是 5(转换次数)* For 循环的时间复杂度;由于多个转换操作只会在 Terminal 操作的时候融合起来,也就是只会在b操作的执行的时候一并完成,即完成该示例的时间复杂度就等于 一次For 循环的时间复杂度。
这里再给一个流操作的示例:
int sum = widgets.stream() // 生成流 stream
.filter(w -> w.getColor() == RED) // 筛选
.mapToInt(w -> w.getWeight()) // 集合转换
.sum(); // 求和
>>>>>>>> Stream 流的使用