Stream是什么

在探讨探究stream的实现原理和动手实现之前,我们先要体会stream流计算的独特之处。

  举个例子: 有一个List<Person>列表,我们需要获得年龄为70岁的前10个Person的姓名。

过程式的解决方案:

  稍加思考,我们很快就写出了一个过程式的解决方案(伪代码):

List<Person> personList = fromDB(); // 获得List<Person>
int limit = 10; // 限制条件
List<String> nameList = new ArrayList(); // 收集的姓名集合
for(Person personItem : personList){if(personItem.age == 70){ // 满足条件nameList.add(personItem.name); // 加入姓名集合if(nameList.size() >= 10){ // 判断是否超过限制break;}}
}
return nameList;

函数式stream解决方案:

  下面我们给出一种基于stream流的解决方案(伪代码):

List<Person> personList = fromDB(); // 获得List<Person>
List<String> nameList = personList.stream().filter(item->item.age == 70) // 过滤条件.limit(10)    // limit限制条件.map(item->item.name) // 获得姓名.collect(Collector.toList()); // 转化为listreturn nameList;

Stream是Java8中新加入的api,更准确的说:

Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,或者大批量数据操作 。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性.

以前我们处理复杂的数据只能通过各种for循环,不仅不美观,而且时间长了以后可能自己都看不太明白以前的代码了,但有Stream以后,通过filter,map,limit等等方法就可以使代码更加简洁并且更加语义化。

特点:
         只遍历一次:一个流只能遍历一次
         内部迭代:steams库使用内部迭代把迭代做了,不需要通过用户编写代码进行外部迭代

并行遍历时,数据会被分为多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。使用串行遍历时,迭代元素依次执行每一个操作后,再迭代下一个元素

流的构成

  • 零个或多个中间操作
  • 终止操作

Stream中的操作可以分为两大类

中间操作与结束操作,中间操作只是对操作进行了记录,只有结束操作才会触发实际的计算(即惰性求值),这也是Stream在迭代大集合时高效的原因之一。中间操作又可以分为无状态(Stateless)操作与有状态(Stateful)操作,前者是指元素的处理不受之前元素的影响;后者是指该操作只有拿到所有元素之后才能继续下去。结束操作又可以分为短路与非短路操作,这个应该很好理解,前者是指遇到某些符合条件的元素就可以得到最终结果;而后者是指必须处理所有元素才能得到最终结果。

API使用

一. 中间操作

我们定义一个简单的学生实体类,用于后面的例子演示:

public class Student {/** 学号 */private long id;private String name;private int age;/** 年级 */private int grade;/** 专业 */private String major;/** 学校 */private String school;// 省略getter和setter
}// 初始化
List<Student> students = new ArrayList<Student>() {{add(new Student(20160001, "孔明", 20, 1, "土木工程", "武汉大学"));add(new Student(20160002, "伯约", 21, 2, "信息安全", "武汉大学"));add(new Student(20160003, "玄德", 22, 3, "经济管理", "武汉大学"));add(new Student(20160004, "云长", 21, 2, "信息安全", "武汉大学"));add(new Student(20161001, "翼德", 21, 2, "机械与自动化", "华中科技大学"));add(new Student(20161002, "元直", 23, 4, "土木工程", "华中科技大学"));add(new Student(20161003, "奉孝", 23, 4, "计算机科学", "华中科技大学"));add(new Student(20162001, "仲谋", 22, 3, "土木工程", "浙江大学"));add(new Student(20162002, "鲁肃", 23, 4, "计算机科学", "浙江大学"));add(new Student(20163001, "丁奉", 24, 5, "土木工程", "南京大学"));}
};
  • 过滤

过滤,顾名思义就是按照给定的要求对集合进行筛选满足条件的元素,java8提供的筛选操作包括:filter、distinct、limit、skip。

filter
在前面的例子中我们已经演示了如何使用filter,其定义为:Stream<T> filter(Predicate<? super T> predicate),filter接受一个谓词Predicate,我们可以通过这个谓词定义筛选条件,在介绍lambda表达式时我们介绍过Predicate是一个函数式接口,其包含一个test(T t)方法,该方法返回boolean。现在我们希望从集合students中筛选出所有武汉大学的学生,那么我们可以通过filter来实现,并将筛选操作作为参数传递给filter:

List<Student> whuStudents = students.stream().filter(student -> "武汉大学".equals(student.getSchool())).collect(Collectors.toList());

distinct
distinct操作类似于我们在写SQL语句时,添加的DISTINCT关键字,用于去重处理,distinct基于Object.equals(Object)实现,回到最开始的例子,假设我们希望筛选出所有不重复的偶数,那么可以添加distinct操作:

List<Integer> evens = nums.stream().filter(num -> num % 2 == 0).distinct().collect(Collectors.toList());

limit
limit操作也类似于SQL语句中的LIMIT关键字,不过相对功能较弱,limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度,比如下面的例子返回前两个专业为土木工程专业的学生:

List<Student> civilStudents = students.stream().filter(student -> "土木工程".equals(student.getMajor())).limit(2).collect(Collectors.toList());

说到limit,不得不提及一下另外一个流操作:sorted。该操作用于对流中元素进行排序,sorted要求待比较的元素必须实现Comparable接口,如果没有实现也不要紧,我们可以将比较器作为参数传递给sorted(Comparator<? super T> comparator),比如我们希望筛选出专业为土木工程的学生,并按年龄从小到大排序,筛选出年龄最小的两个学生,那么可以实现为:

List<Student> sortedCivilStudents = students.stream().filter(student -> "土木工程".equals(student.getMajor())).sorted((s1, s2) -> s1.getAge() - s2.getAge()).limit(2).collect(Collectors.toList());

skip
skip操作与limit操作相反,如同其字面意思一样,是跳过前n个元素,比如我们希望找出排序在2之后的土木工程专业的学生,那么可以实现为:

List<Student> civilStudents = students.stream().filter(student -> "土木工程".equals(student.getMajor())).skip(2).collect(Collectors.toList());

通过skip,就会跳过前面两个元素,返回由后面所有元素构造的流,如果n大于满足条件的集合的长度,则会返回一个空的集合。

  • 映射

在SQL中,借助SELECT关键字后面添加需要的字段名称,可以仅输出我们需要的字段数据,而流式处理的映射操作也是实现这一目的,在java8的流式处理中,主要包含两类映射操作:map和flatMap。

map
举例说明,假设我们希望筛选出所有专业为计算机科学的学生姓名,那么我们可以在filter筛选的基础之上,通过map将学生实体映射成为学生姓名字符串,具体实现如下:

List<String> names = students.stream().filter(student -> "计算机科学".equals(student.getMajor())).map(Student::getName).collect(Collectors.toList());

除了上面这类基础的map,java8还提供了mapToDouble(ToDoubleFunction<? super T> mapper)mapToInt(ToIntFunction<? super T> mapper)mapToLong(ToLongFunction<? super T> mapper),这些映射分别返回对应类型的流,java8为这些流设定了一些特殊的操作,比如我们希望计算所有专业为计算机科学学生的年龄之和,那么我们可以实现如下:

int totalAge = students.stream().filter(student -> "计算机科学".equals(student.getMajor())).mapToInt(Student::getAge).sum();

通过将Student按照年龄直接映射为IntStream,我们可以直接调用提供的sum()方法来达到目的,此外使用这些数值流的好处还在于可以避免jvm装箱操作所带来的性能消耗。

flatMap
flatMap与map的区别在于 flatMap是将一个流中的每个值都转成一个个流,然后再将这些流扁平化成为一个流 。举例说明,假设我们有一个字符串数组String[] strs = {"java8", "is", "easy", "to", "use"};,我们希望输出构成这一数组的所有非重复字符,那么我们可能首先会想到如下实现:

List<String[]> distinctStrs = Arrays.stream(strs).map(str -> str.split(""))  // 映射成为Stream<String[]>.distinct().collect(Collectors.toList());

在执行map操作以后,我们得到是一个包含多个字符串(构成一个字符串的字符数组)的流,此时执行distinct操作是基于在这些字符串数组之间的对比,所以达不到我们希望的目的,此时的输出为:

[j, a, v, a, 8]
[i, s]
[e, a, s, y]
[t, o]
[u, s, e]

distinct只有对于一个包含多个字符的流进行操作才能达到我们的目的,即对Stream<String>进行操作。此时flatMap就可以达到我们的目的:

List<String> distinctStrs = Arrays.stream(strs).map(str -> str.split(""))  // 映射成为Stream<String[]>.flatMap(Arrays::stream)  // 扁平化为Stream<String>.distinct().collect(Collectors.toList());

flatMap将由map映射得到的Stream<String[]>,转换成由各个字符串数组映射成的流Stream<String>,再将这些小的流扁平化成为一个由所有字符串构成的大流Steam<String>,从而能够达到我们的目的。
与map类似,flatMap也提供了针对特定类型的映射操作:flatMapToDouble(Function<? super T,? extends DoubleStream> mapper)flatMapToInt(Function<? super T,? extends IntStream> mapper)flatMapToLong(Function<? super T,? extends LongStream> mapper)

二. 终端操作

终端操作是流式处理的最后一步,我们可以在终端操作中实现对流查找、归约等操作。

  • 查找

allMatch
allMatch用于检测是否全部都满足指定的参数行为,如果全部满足则返回true,例如我们希望检测是否所有的学生都已满18周岁,那么可以实现为:

boolean isAdult = students.stream().allMatch(student -> student.getAge() >= 18);

anyMatch
anyMatch则是检测是否存在一个或多个满足指定的参数行为,如果满足则返回true,例如我们希望检测是否有来自武汉大学的学生,那么可以实现为:

boolean hasWhu = students.stream().anyMatch(student -> "武汉大学".equals(student.getSchool()));

noneMathch
noneMatch用于检测是否不存在满足指定行为的元素,如果不存在则返回true,例如我们希望检测是否不存在专业为计算机科学的学生,可以实现如下:

boolean noneCs = students.stream().noneMatch(student -> "计算机科学".equals(student.getMajor()));

findFirst
findFirst用于返回满足条件的第一个元素,比如我们希望选出专业为土木工程的排在第一个学生,那么可以实现如下:

Optional<Student> optStu = students.stream().filter(student -> "土木工程".equals(student.getMajor())).findFirst();

findFirst不携带参数,具体的查找条件可以通过filter设置,此外我们可以发现findFirst返回的是一个Optional类型,关于该类型的具体讲解可以参考上一篇:Java8新特性 – Optional类。

findAny
findAny相对于findFirst的区别在于,findAny不一定返回第一个,而是返回任意一个,比如我们希望返回任意一个专业为土木工程的学生,可以实现如下:

Optional<Student> optStu = students.stream().filter(student -> "土木工程".equals(student.getMajor())).findAny();

实际上对于顺序流式处理而言,findFirst和findAny返回的结果是一样的,至于为什么会这样设计,是因为在下一篇我们介绍的并行流式处理,当我们启用并行流式处理的时候,查找第一个元素往往会有很多限制,如果不是特别需求,在并行流式处理中使用findAny的性能要比findFirst好。

  • 归约

前面的例子中我们大部分都是通过collect(Collectors.toList())对数据封装返回,如我的目标不是返回一个新的集合,而是希望对经过参数化操作后的集合进行进一步的运算,那么我们可用对集合实施归约操作。java8的流式处理提供了reduce方法来达到这一目的。

前面我们通过mapToInt将Stream<Student>映射成为IntStream,并通过IntStream的sum方法求得所有学生的年龄之和,实际上我们通过归约操作,也可以达到这一目的,实现如下:

// 前面例子中的方法
int totalAge = students.stream().filter(student -> "计算机科学".equals(student.getMajor())).mapToInt(Student::getAge).sum();
// 归约操作
int totalAge = students.stream().filter(student -> "计算机科学".equals(student.getMajor())).map(Student::getAge).reduce(0, (a, b) -> a + b);// 进一步简化
int totalAge2 = students.stream().filter(student -> "计算机科学".equals(student.getMajor())).map(Student::getAge).reduce(0, Integer::sum);// 采用无初始值的重载版本,需要注意返回Optional
Optional<Integer> totalAge = students.stream().filter(student -> "计算机科学".equals(student.getMajor())).map(Student::getAge).reduce(Integer::sum);  // 去掉初始值
  • 收集

前面利用collect(Collectors.toList())是一个简单的收集操作,是对处理结果的封装,对应的还有toSettoMap,以满足我们对于结果组织的需求。这些方法均来自于java.util.stream.Collectors,我们可以称之为收集器。

 归约

收集器也提供了相应的归约操作,但是与reduce在内部实现上是有区别的,收集器更加适用于可变容器上的归约操作,这些收集器广义上均基于Collectors.reducing()实现。

例1:求学生的总人数long count = students.stream().collect(Collectors.counting());// 进一步简化
long count = students.stream().count();
例2:求年龄的最大值和最小值// 求最大年龄
Optional<Student> olderStudent = students.stream().collect(Collectors.maxBy((s1, s2) -> s1.getAge() - s2.getAge()));// 进一步简化
Optional<Student> olderStudent2 = students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge)));// 求最小年龄
Optional<Student> olderStudent3 = students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge)));
例3:求年龄总和int totalAge4 = students.stream().collect(Collectors.summingInt(Student::getAge));
对应的还有summingLong、summingDouble。例4:求年龄的平均值double avgAge = students.stream().collect(Collectors.averagingInt(Student::getAge));
对应的还有averagingLong、averagingDouble。例5:一次性得到元素个数、总和、均值、最大值、最小值IntSummaryStatistics statistics = students.stream().collect(Collectors.summarizingInt(Student::getAge));
输出:IntSummaryStatistics{count=10, sum=220, min=20, average=22.000000, max=24}
对应的还有summarizingLong、summarizingDouble。例6:字符串拼接String names = students.stream().map(Student::getName).collect(Collectors.joining());
// 输出:孔明伯约玄德云长翼德元直奉孝仲谋鲁肃丁奉
String names = students.stream().map(Student::getName).collect(Collectors.joining(", "));
// 输出:孔明, 伯约, 玄德, 云长, 翼德, 元直, 奉孝, 仲谋, 鲁肃, 丁奉

 分组

在数据库操作中,我们可以通过GROUP BY关键字对查询到的数据进行分组,java8的流式处理也为我们提供了这样的功能Collectors.groupingBy来操作集合。比如我们可以按学校对上面的学生进行分组:

Map<String, List<Student>> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool));
groupingBy接收一个分类器Function<? super T, ? extends K> classifier,我们可以自定义分类器来实现需要的分类效果。

上面演示的是一级分组,我们还可以定义多个分类器实现 多级分组,比如我们希望在按学校分组的基础之上再按照专业进行分组,实现如下:

Map<String, Map<String, List<Student>>> groups2 = students.stream().collect(Collectors.groupingBy(Student::getSchool,  // 一级分组,按学校Collectors.groupingBy(Student::getMajor)));  // 二级分组,按专业

实际上在groupingBy的第二个参数不是只能传递groupingBy,还可以传递任意Collector类型,比如我们可以传递一个Collector.counting,用以统计每个组的个数:

Map<String, Long> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool, Collectors.counting()));

如果我们不添加第二个参数,则编译器会默认帮我们添加一个Collectors.toList()

 分区

分区可以看做是分组的一种特殊情况,在分区中key只有两种情况:true或false,目的是将待分区集合按照条件一分为二,java8的流式处理利用collectors.partitioningBy()方法实现分区,该方法接收一个谓词,例如我们希望将学生分为武大学生和非武大学生,那么可以实现如下:

Map<Boolean, List<Student>> partition = students.stream().collect(Collectors.partitioningBy(student -> "武汉大学".equals(student.getSchool())));

分区相对分组的优势在于,我们可以同时得到两类结果,在一些应用场景下可以一步得到我们需要的所有结果,比如将数组分为奇数和偶数。

java8 Stream详解相关推荐

  1. Java8 Stream详解~ 提取/组合

    流也可以进行合并.去重.限制.跳过等操作. public class StreamTest {public static void main(String[] args) {String[] arr1 ...

  2. Java8 Stream详解~排序:sorted

    sorted,中间操作.有两种排序: sorted():自然排序,流中元素需实现Comparable接口 sorted(Comparator com):Comparator排序器自定义排序 「案例:将 ...

  3. Java8 Stream详解~收集(collect)

    collect,收集,可以说是内容最繁多.功能最丰富的部分了.从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合. 1 归集(toList/toSet/toMap) ...

  4. Java8 Stream详解~归约(reduce)

    归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和.求乘积和求最值操作. 「案例一:求Integer集合的元素之和.乘积和最大值.」 public class StreamTest { ...

  5. Java8 Stream详解~映射(map/flatMap)

    映射,可以将一个流的元素按照一定的映射规则映射到另一个流中.分为map和flatMap: map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素. flatMap:接收一个 ...

  6. Java8 Stream详解~聚合(max/min/count)

    max.min.count这些字眼你一定不陌生,没错,在mysql中我们常用它们进行数据统计.Java stream中也引入了这些概念和用法,极大地方便了我们对集合.数组的数据统计工作. 「案例一:获 ...

  7. Java8 Stream详解~筛选:filter

    筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作. 「案例一:筛选出Integer集合中大于7的元素,并打印出来」 public class StreamTest {publ ...

  8. Java8 Stream详解~遍历/匹配(foreach/find/match)

    Stream也是支持类似集合的遍历和匹配元素的,只是Stream中的元素是以Optional类型存在的.Stream的遍历.匹配非常简单. // import已省略,请自行添加,后面代码亦是publi ...

  9. Java8 Stream详解~Stream 创建

    Stream可以通过集合数组创建. 1.通过 java.util.Collection.stream() 方法用集合创建流 List<String> list = Arrays.asLis ...

  10. Java8 Stream详解~Stream概述

    Java 8 是一个非常成功的版本,这个版本新增的Stream,配合同版本出现的 Lambda ,给我们操作集合(Collection)提供了极大的便利. 那么什么是Stream? Stream将要处 ...

最新文章

  1. ubuntu15.10避免图形界面无法登录的jdk配置
  2. 即时与及时有什么区别_什么是即时配送它和快递有什么不同,镖滴新势力
  3. bzoj2754: [SCOI2012]喵星球上的点名
  4. 使用Opencv将RGB颜色空间转换到HSV颜色空间/灰度图
  5. python 给字符串加颜色
  6. C语言实例--百钱买百鸡
  7. 【孪生网络siamfc代码学习】
  8. springboot webService调用
  9. thrift 编译报错 undefined reference
  10. 【Python 数据科学】分组group by基础
  11. 【毕业设计】基于机器学习的餐厅销量预测 -大数据 python
  12. 为什么需要序列化总结
  13. 别总说CMS、G1,该聊聊ZGC了
  14. 为什么调用Dao层会报空指针异常
  15. 用HBuild生成APP启动页面准备
  16. AI中怎么给文字加粗
  17. 【腾讯开放平台】Android、IOS实现指定QQ临时会话功能
  18. makefile初识
  19. 灭霸消灭一般人口是随即的吗_是时候消灭皇家战斗风格了
  20. Simulink快速入门:如何搭建仿真模型——以双向直流变换器为例(附模型)

热门文章

  1. 【kali】一款黑客们都在使用的操作系统
  2. 关于 Jupyter Notebook 中 No module named ‘torch‘ 的解决办法
  3. FTP+Cyberduck+服务器部署项目+pm2
  4. Lab4 Architecture Lab
  5. Laravel之数据库操作与Eloquent模型使用总结
  6. 51单片机系列--中断系统
  7. 车轮轨迹原理_三张图告诉你倒车入库的原理
  8. 【HDOJ】1814 Peaceful Commission
  9. jackson解析泛型的正确写法,解决 in unnamed module of loader ‘app‘
  10. 倾斜模型精细化处理_浅谈几个倾斜摄影三维模型的修补软件 - 纳金网