Stream概述
Stream
是Java 8
继lambda表达式
的又一大重要更新,很多人开始使用Java 8
最直接的需求就是基于Stream
这套API
,函数式编程可能比较抽象,Stream API
的设计基于函数式编程
和lambda表达式
,有力地展现了函数式编程
和lambda表达式
结合带给Java
的强大能力,同时又不需要太高的技术门槛即可享受到所带来的好处。
集合框架
集合是Java
中使用最多的API
,要是没有集合,还能做什么呢?几乎每个Java
应用程序都会制造和处理集合。集合对于很多编程任务来说都是非常基本的。尽管集合对于几乎任何一个Java
应用都是不可或缺的,但集合操作却远远算不上完美。
什么是集合?
集合
可以看成是对编程开发中经常使用到的数据结构与算法
的封装,数据结构解决如何有效地组织和操作数据,这些数据结构通常称为Java集合框架
。
业务系统开发有一个比较通俗的叫法:面向数据库开发,基于数据库的存储模型和SQL语法
实现对数据的存储及各种数据处理操作,SQL
开发在业务系统开发中占有很大的比例。但是数据库会存在网路IO
和磁盘IO
在性能上会存在问题。
如果我们站在一种更高抽象层次上来说,集合其实就可以看成是一种微型数据库
,它们关注点都是解决如何有效地组织数据和操作数据,直白点将就是:把存储数据存储起来并可以数据进行增删改查
等操作。
集合数据处理能力不足
很多业务逻辑都涉及类似于数据库的操作,比如对学生成绩按照科目分组,找出每个科目成绩最高分:select subject, max(score) from students group by subject
。你只需要表达你想要什么,而不需要关注具体如何去做。这个基本的思路意味着,你用不着担心怎么去显式地实现这些查询语句——都替你办好了!怎么到了集合这里就不能这样了呢?
如上图,集合框架顶层接口Collection所包含的方法列表:获取集合元素数量、集合是否为空、集合是否包含某个元素、添加元素、移除元素等很基本操作,和数据库提供的SQL特性相比,集合在数据处理能力上简直弱爆了。比如:select name from student where score < 60 and name = 'lisi' order by score, age desc
,简单、直白的完成了将对数据的多种操作组合在一起。
集合并发处理复杂
要是要处理大量元素又该怎么办呢?为了提高性能,你需要并行处理,并利用多核架构。但写并行代码比用迭代器还要复杂,而且调试起来也够受的!那Java
语言的设计者能做些什么,来帮助你节约宝贵的时间,让你这个程序员活得轻松一点儿呢?
基本概念
Stream API
可以把它看成一种高级迭代器
,提供声明式
处理数据集合能力,使用类似于数据库的操作帮助你处理集合。它支持两种类型的操作:中间操作和终端操作。中间操作可以链接起来,将一个流转成另一个流,这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗流,以产生一个最终结果,它们通常可以通过优化流水线来缩短计算时间。
区分中间操作和终止操作就是看返回类型:中间操作都会返回一个Stream
对象,而终止操作则不返回Stream
类型,可能不返回值,也可能返回其它类型的单个值。中间操作返回类型是Stream
,可以进行串联形成流水线;中间操作具有惰性求值特性,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理。
map
使用map 操作将字符串转换为大写形式:
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase())
.collect(toList());
filter
List<String> beginningWithNumbers =
Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
flatMap
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
reduce(规约)
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element);
collect(收集器)
数据分块
public Map<Boolean, List<Artist>> bandsAndSoloRef(Stream<Artist> artists) {
return artists.collect(partitioningBy(Artist::isSolo));
}
数据分组
使用主唱对专辑分组:
public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) {
return albums.collect(groupingBy(album -> album.getMainMusician()));
}
类似于SQL中的group by 操作,我们的方法是和这类似的一个概念,只不过在Stream 类库中实现了而已
字符串
很多时候,收集流中的数据都是为了在最后生成一个字符串。
String result = artists.stream()
.map(Artist::getName)
.collect(Collectors.joining(", ", "[", "]"));
结果样本::[George Harrison, John Lennon, Paul McCartney, Ringo Starr, The Beatles]
内部迭代和外部迭代
集合和流的一个关键区别在于它们遍历数据的方式不同 ,使用Collection API
你得用for-each
或Iterator迭代器
一个个去获取元素,拿到元素后续的处理流程完全靠开发人员自己处理,这称为外部迭代。有了Stream API
后,你根本用不着操心循环迭代的事情,数据处理流程完全是在Stream库
内部驱动进行,这种方式称为内部迭代。 下面的代码列表说明了这种区别。
List<String> names = new ArrayList<>();
for(Dish d: menu){
names.add(d.getName());
}
List<String> names = menu.stream()
.map(Dish::getName)
.collect(toList());
内部迭代与外部迭代示意图
内部迭代
和外部迭代
的本质区别:驱动方式
发生了转变,内部迭代
驱动方变成了Stream库
;而外部迭代
驱动方在是程序开发人员,就好比Spring
中控制反转
概念一样,这种驱动方式转变意义在哪里呢?下面继续通过一个案例说明。
int ret = list.stream()
.filter(x -> x > 2)
.mapToInt(x -> x * 2)
.skip(2)
.limit(2)
.sum();
数据处理常用的处理模式:组件化+处理链,如上述案例,Stream
会帮你把各种操作组件化
,并将创建的组件
构建成一条处理链
,如果把这个看成一个需求开发的话,开发人员把只需要把需求点告诉Stream API
,由Stream API
来构建完整的数据处理流程,并在合适的时机通过回调方式将需求点的业务逻辑嵌入到处理流程中即可。
如果使用传统的集合API
中for-each
或Iterator迭代器
循环一个个去迭代元素,后续如何组件化
以及将这些组件
构建成处理链
完全有开发人员自己开发维护。一个标准组件(见下图)真正的业务逻辑部分只是其中一部分,如:是否包含一个Queue进行组件间解耦、是否使用线程池进行并发控制、如何将处理后的数据传递到下一个组件等等。这些其实与业务逻辑没有太大关联性,但是又是每个组件必须考虑的公共特性。
本身开发难度非常高,再加上开发人员能力水平参差不齐,很容易导致开发出来的代码质量不高、扩展不强、后期很难维护等。Stream
就是看到集合存在的问题,由它来帮你如何设计组件以及如何构建处理链,开发人员就好比甲方提出你的需求,由Stream来设计、开发。
Stream API
是由JDK
高级开发工程师实现的(开发人员能力得到保证),而且在全世界范围广泛使用(代码质量得到保证),Stream
作为驱动方,尽可能的帮你做了代码优化,比如这里另一个最大收益就是并行开发。
比如现在处理的集合数据量非常大,服务器是多核处理器,理想情况下,你可能会让这些CPU内核共同分担处理工作,以缩短处理时间。问题在于,通过多线程代码来利用并行(使用先前Java版本中的Thread API)并非易事。你得换一种思路:线程可能会同时访问并更新共享变量。因此,如果没有协调好,数据可能会被意外改变。
Long sum = LongStream.rangeClosed(1, 100)
.parallel()
.sum();
Stream API
和Collection API
通俗例子说明:
公司入职一批新员工要求做自我介绍,有的人表达能力强介绍的很精彩,有的人可能就很茫然不晓得咋个介绍等情况,最后导致大家介绍样式五花八门。现在给新员工统一一份自我介绍模板,只需要把各人的如姓名、工作年限等基本信息填入即可。Stream API
就像是这份模板,而开发人员只需要做简单的填空,而集合API
就像第一种情况,给你一个需求,完全靠开发人员自由发挥,显然做填空题的难度和工作量要小很多。
Stream和Collection区别
很多刚接触Stream API的,很容易把Stream和Collection概念混淆:Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算处理,即集合关注的是数据和数据存储本身,而流关注的则是对数据的计算。
声明式编程
业务系统开发中SQL
使用很高的另一个很重要的原因是:SQL
作为一种声明式语言,通过简单的声明式指令即可实现数据处理,不需要过多的关系底层的实现细节,降低了开发难度,提高了开发效率。Stream也是遵循:做什么,而不用关心怎么去做的原则。
SQL语句是一种描述性语言:
select name from student where score < 60 and name = 'lisi' order by score, age desc;
studentList.stream()
.filter(student -> student.getScore() < 60)
.filter(student -> student.getName().equals("lisi"))
.sorted(Comparator.comparingInt(Student::getScore).thenComparingInt(Student::getAge).reversed())
.map(Student::getName)
.collect(Collectors.toList());
总结
集合关注的是数据和数据存储本身,而流关注的则是对数据的计算,流不是替代集合的,流是为了给集合更好的服务。
Stream
可以用类似于数据库的操作帮助你处理集合,Stream就是将集合这一面向“存储”的对象赋予了面向“计算”的能力。