文章目录
- 一、流式处理简介
- 二、中间操作
- 1.过滤
- 2.映射
- 三、终端操作
- 1.查找
- 2.归约
- 3.收集
- 四、并行流式数据处理
一、流式处理简介
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。使用流式处理可以大大简化代码。
比如我们希望筛选出一个整数集合中所有偶数
不使用java8
List<Integer> evens = new ArrayList<>();
for (Integer num : nums) {
if ((num & 1) == 0) {
evens.add(num);
}
}
通过流式处理
List<Integer> evens = nums.stream().filter(num -> (num & 1) == 0).collect(Collectors.toList());
先简单说明下上面语句的含义,stream()是将集合转换成一个流,filter()执行我们自定义的筛选处理,最后我们通过colllect()对结果进行封装处理,并通过Collectors.toList()指定其封装为一个List。
java8的流式处理极大的简化了对于集合的操作,实际上不仅是集合,包括数组、文件等,只要可以转换称为流,我们都可以借助流式处理。java8是通过内部迭代来实现对流的处理,一个流式处理可以分为三个部分:转换成流、中间操作和终端操作。
二、中间操作
为了方便演示,我们先定义一个简单的学生实体类
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, "土木工程", "南京大学"));
}
};
1.过滤
过滤,即对集合进行筛选,java8提供的筛选操作包括:filter、distinct、limit、skip。
- filter
Stream filter(Predicate<? super T> predicate),filter接受一个谓词Predicate,Predicate是一个函数式接口,其包含一个test(T t)方法,该方法返回boolean。现在我们希望从集合students中筛选出所有武汉大学的学生:
List<Student> whuStudents = students.stream()
.filter(student -> "武汉大学".equals(student.getSchool()))
.collect(Collectors.toList());
- distinct
distinct操作类似与我们在写SQL语句时,添加的distinct关键字,用于去重处理,distinct基于Object.equals(Object)实现,回到最开始的例子,假如我们希望筛选出不重复的偶数:
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大于满足条件的集合的长度,则会返回一个空的集合。
skip和limit可以类比于SQL中的offset和limit。
students.stream().skip(m).limit(n)
可以等价于
students.subList(m, m+n);
不过流式处理在越界时,不会抛异常,而是返回空集合或者到集合结束位置。
2.映射
主要包括两种映射操作:map和flatMap
- map
假如我们希望筛选出所有专业为计算机科学的学生姓名
students.stream().filter(student -> "计算机科学".equals(student.getMajor()))
.map(Student::getName).collect(Collectors.toList())
此外,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是将一个流中的每个值转成一个个流,然后再将这些流扁平化成为一个流,假如我们有一个字符数组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]
我们应该使用flatMap
List<String> distinctStrs = Arrays.stream(strs)
.map(str -> str.split("")) // 映射成为Stream<String[]>
.flatMap(Arrays::stream) // 扁平化为Stream<String>
.distinct()
.collect(Collectors.toList());
与map类似,flatMap也提供了针对特定类型的映射操作:flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper)…
三、终端操作
终端操作是流式处理的最后一步,我们可以在终端操作中实现对流查找、归约等操作。
1.查找
- allMatch
allMatch用于检测是否全部都满足指定条件;例如我们希望检测是否所有的学生都已满18周岁
boolean isAudit = students.stream().allMatch(student -> student.getAge() > 18);
- anyMatch
与allMatch类似,只要存在一个满足就返回true - noneMatch
当全部都不满足条件时返回true - findFirst
- findAny
2.归约
大部分情况我们都是通过collect(Collectors.toList())对数据封装返回,如果我们的目标不是返回一个集合,而是希望对经过参数化操作后的集合进行进一步计算,那么我们可用对集合实施归约操作。
前面我们通过mapToInt将Stream映射成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 totalAge = students.stream()
.filter(student -> "计算机科学".equals(student.getMajor()))
.map(Student::getAge)
.reduce(0, Integer::sum);
3.收集
前面利用collect(Collectors.toList())是一个简单的收集操作,是对处理结果的封装,对应的还有toSet、toMap,以满足我们对于结果组织的需求。这些方法均来自于java.util.stream.Collectors,我们可以称之为收集器。
- 归约
收集器也提供了相应的归约操作,但是与reduce在内部实现上是有区别的,收集器更加适用于可变容器上的归约操作,这些收集器广义上均基于Collectors.reducing()实现。
求学生总人数
long count = students.stream().collect(Collectors.counting());
// 进一步简化
long count = students.stream().count();
求年龄的最大值和最小值
// 求最大年龄
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)));
求年龄总和
int totalAge = students.stream().collect(Collectors.summingInt(Student::getAge));
对应的还有summingLong、summingDouble
求年龄平均值
double avgAge = students.stream().collect(Collectors.averagingInt(Student::getAge));
一次性得到元素个数、总和、均值、最大值和最小值
IntSummaryStatistics statistics = students.stream().collect(Collectors.summarizingInt(Student::getAge));
输出:
IntSummaryStatistics{count=10, sum=220, min=20, average=22.000000, max=24}
字符串拼接
String names = students.stream().map(Student::getName).collect(Collectors.joining());
// 输出:孔明伯约玄德云长翼德元直奉孝仲谋鲁肃丁奉
String names = students.stream().map(Student::getName).collect(Collectors.joining(", "));
// 输出:孔明, 伯约, 玄德, 云长, 翼德, 元直, 奉孝, 仲谋, 鲁肃, 丁奉
- 分组
java8流式处理为我们提供了Collectors.groupingBy来对集合进行分组。比如我们可以按学校对上面的学生进行分组
Map<String, List<Student>> groups = students.stream().collect(Collectors.groupingBy(Student::getSchool));
我们还可以定义多个分类器实现多级分组,比如我们希望在按学校分组的基础上再按照专业进行分组
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()));
如果我们不添加第二个参数,则编译器会默认帮我们添加一个Collector.toList()。
- 分区
分区可以看做是分组的一种特殊情况,在分区中key只有两种情况:true或false,目的是将待分区集合按照条件一分为二,java8的流式处理利用ollectors.partitioningBy()方法实现分区,该方法接收一个谓词,例如我们希望将学生分为武大学生和非武大学生,那么可以实现如下:
Map<Boolean, List<Student>> partition = students.stream()
.collect(Collectors.partitioningBy(student -> "武汉大学".equals(student.getSchool())));
分区相对分组的优势在于,我们可以同时得到两类结果,在一些应用场景下可以一步得到我们需要的所有结果,比如将数组分为奇数和偶数。
四、并行流式数据处理
流式处理中的很多都适合采用 分而治之 的思想,从而在处理集合较大时,极大的提高代码的性能,java8的设计者也看到了这一点,所以提供了 并行流式处理。上面的例子中我们都是调用stream()方法来启动流式处理,java8还提供了parallelStream()来启动并行流式处理,parallelStream()本质上基于java7的Fork-Join框架实现,其默认的线程数为宿主机的内核数。
启动并行流式处理虽然简单,只需要将stream()替换成parallelStream()即可,但既然是并行,就会涉及到多线程安全问题,所以在启用之前要先确认并行是否值得(并行的效率不一定高于顺序执行),另外就是要保证线程安全。此两项无法保证,那么并行毫无意义,毕竟结果比速度更加重要。