文章目录

  • 一、流式处理简介
  • 二、中间操作
  • 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是通过内部迭代来实现对流的处理,一个流式处理可以分为三个部分:转换成流、中间操作和终端操作。

java流式处理原理 java流式处理原理是什么_java

二、中间操作

为了方便演示,我们先定义一个简单的学生实体类

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()即可,但既然是并行,就会涉及到多线程安全问题,所以在启用之前要先确认并行是否值得(并行的效率不一定高于顺序执行),另外就是要保证线程安全。此两项无法保证,那么并行毫无意义,毕竟结果比速度更加重要。