第2节 Java8 Stream 流编程(Lambda 表达式) 2021-12-29

Java组件总目录

1 Stream 概述

它属于结构化编程的一种,主要思想是把对数据的一连串操作通过链式函数调用的方式来实现。

JDK8 中,Stream 是一个接口,其中包含了很多的方法,这些方法可以对流中的数据进行操作。这些方法共分为三类:

  • 创建 Stream 接口对象的方法,这些方法一般都需要通过输入原始操作数,然后创建数据池
  • 返回 Stream 类型的中间操作方法
  • 返回其它类型的方法,这些方法即终止操作方法

1 注意的问题

  • 中间操作返回的都是 Stream,可以保证“链式调用”。
  • 这些函数都有一个共同的任务:对 Stream 流中的每一个数据都进行操作
  • 一个中间函数对于数据池的操作结果将作为下一个中间操作函数的数据输入
  • 所有操作都是不可逆的,一旦操作就会影响数据池中的数据

2 惰性求值

Stream 流编程中对于中间方法的执行,存在一个惰性求值问题:多个中间操作可以连接起来形成一个链式调用,除非链式调用的最后执行了终止操作,否则中间操作是不会执行任何处理的。即只有当终止操作执行时才会触发链式调用的执行,这种方法调用方式称为惰性求值。

2 Stream 流的创建

  • 使用数组创建流
  • 使用集合创建流
  • 创建数字流
  • 自定义流
public class Test2 {

    // 使用数组创建流
    @Test
    public void test01() {
        int[] nums = {1,2,3};

        IntStream stream = IntStream.of(nums);
        IntStream stream1 = Arrays.stream(nums);
    }

    // 使用集合创建流
    @Test
    public void test02() {
        // 使用List创建流
        List<String> names = new ArrayList<>();
        Stream<String> stream = names.stream();

        // 使用Set创建流
        Set<String> cities = new HashSet<>();
        Stream<String> stream1 = cities.stream();
    }

    // 创建数字流
    @Test
    public void test03() {
        // 创建一个包含1,2,3的Stream
        IntStream stream = IntStream.of(1, 2, 3);

        // 创建一个包含[1,5)范围的Stream
        IntStream range1 = IntStream.range(1, 5);
        // 创建一个包含[1,5]范围的Stream
        IntStream range2 = IntStream.rangeClosed(1, 5);

        // new Random().ints()创建一个无限流
        // limit(5)限制流中元素个数为5个
        IntStream ints = new Random().ints().limit(5);
        // 与上面的写法等价
        IntStream ints1 = new Random().ints(5);
    }

    // 自定义流
    @Test
    public void test04() {
        // 首先生成一个无限流,然后又限定了元素个数
        Stream<Double> stream = Stream.generate(() -> Math.random())
                                      .limit(5);
    }

}

3 并行处理与串行处理

我们前面的操作都是由一个线程以串行的方式逐个对流中的元素进行处理的。为了提高处理效率,Stream 支持以并行的方式对流中的元素进行处理。
使用 Stream 流式编程对于并行操作非常简单,无需自己创建 Thread 多线程,只需调用parallel()方法即可实现多线程环境。默认情况下为串行流

parallel()多线程默认使用的线程池为 ForkJoinPool-commonPool,其默认线程数是可以修改的。需要设置该线程池相关的一个系统属性。

  • 串行处理 (默认)
  • 并行流输出 (多个模式以最后设置的模式为准)
  • 线程数修改
  • 使用自定义线程池(若当前系统中多个进程同时使用这个默认的线程池,则很大情况下会形成任务阻塞的状况。)说明:wait(),notify()和 notifyAll()方法必须在同步方法或同步代码块中被调用,且哪个对象调用这些方法,哪个对象就要充当同步锁。
public class Test4 {

    // 静态方法:打印一个元素(黑色)休眠一次
    public static void print(int i) {
        String name = Thread.currentThread().getName();
        System.out.println(i + " -- " + name);
        try {
            TimeUnit.MILLISECONDS.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 静态方法:打印一个元素(红色)休眠一次
    public static void printRed(int i) {
        String name = Thread.currentThread().getName();
        System.err.println(i + " -- " + name);
        try {
            TimeUnit.MILLISECONDS.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 串行处理
    @Test
    public void test01() {
        IntStream.range(1, 100)
                // .sequential()   // 默认
                .peek(Test4::print) // 使用方法引用 Test4::print
                .count();
    }

    // 并行处理
    @Test
    public void test02() {
        IntStream.range(1, 100)
                .parallel()
                .peek(Test4::print)  
                .count();
    }

    // 串并行混合处理:先并行后串行, 最终执行效果为后者:串行处理
    @Test
    public void test03() {
        IntStream.range(1, 100)
                .parallel()            // 并行处理
                .peek(Test4::print)
                .sequential()          // 串行处理
                .peek(Test4::printRed)
                .count();
    }

    // 串并行混合处理:先串行后并行
    // 最终执行效果为后者:并行处理
    @Test
    public void test04() {
        IntStream.range(1, 100)
                .sequential()          // 串行处理
                .peek(Test4::printRed)
                .parallel()            // 并行处理
                .peek(Test4::print)
                .count();
    }

    // 修改默认线程池中的线程数量
    @Test
    public void test05() {
        // 指定默认线程池中的数量为16,其中包含指定的15个与main线程
        String key = "java.util.concurrent.ForkJoinPool.common.parallelism";
        System.setProperty(key, "15");

        IntStream.range(1, 100)
                .parallel()            // 并行处理
                .peek(Test4::print)
                .count();
    }

    // 使用自定义线程池
    @Test
    public void test06() {
        // 创建线程池,包含4个线程
        ForkJoinPool pool = new ForkJoinPool(4);
        pool.submit(() -> IntStream.range(1, 100)
                                    .parallel()            // 并行处理
                                    .peek(Test4::print)
                                    .count()
                    );

        // wait()、notify()、notifyAll()方法必须在同步方法或同步代码块中被调用,
        // 且哪个对象调用了这些方法,哪个对应就要充当同步锁
        synchronized (pool) {
            try {
                // main线程被阻塞在了这里
                pool.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

4 Stream 流的中间操作

返回 Stream 接口类型的方法称为中间操作。根据“这些操作对当前数据池中的某一元素进行操作时是否需要了解对之前元素操作的结果”标准,可以将中间操作划分为无状态操作与有状态操作。

元素跟前后元素是否有关系,没有为无状态操作,仅仅与自身有关。

通常 Lambda 表达式的参数并不需要显示声明类型。那么对于给定的Lambda表达式,程序如何知道对应的是哪个函数接口以及参数的类型呢?编译器通过 Lambda 表达式所在的上下文来进行目标类型推断,通过检查 Lambda 表达式的入参类型及返回类型,和对应的目标类型的方法签名是否一致,推导出合适的函数接口。

Stream.of("foo", "bar").map(s -> s.length()).filter(l -> l == 3);

从 Stream 的类型上下文可以推导出入参是 String 类型,从函数的返回值可以推导出出参是整形类型,因此可推导出对应的函数接口类型为 Function;

1 无状态操作

1 map

其功能是将流中的元素映射为最一个值。由于其参数为 Function,有一个输入与输出。由于有输出,所以 map()操作会流中元素产生影响。

    @Test
    public void test01() {
        String words = "Beijing welcome you I love China";
        // 流中元素逐个拿出,将所有流程执行完,然后执行下一个的。
        Stream.of(words.split(" "))    // 当前流中的元素为各个单词
              .peek(System.out::print)      // peek 功能同forEach, 不改变流中元素, forEach是终结操作,peek不是。
              .map(word -> word.length())   // 当前流中的元素为各个单词的长度
              .forEach(System.out::println);
    }

2 mapToXxx()

其功能是将流中的元素映射为指定类型的元素,不同的流其可映射为的元素类型是不同的,即其所拥有的 mapToXxx()方法是不同的。

    @Test
    public void test02() {
        IntStream.range(1, 10)
                .mapToObj(i -> "No." + i)
                .forEach(System.out::println);
    }

    @Test
    public void test03() {
        String[] nums = {"111", "222", "333"};
        Arrays.stream(nums)   // "111", "222", "333"
                .mapToInt(Integer::valueOf)    // 111, 222, 333
                .forEach(System.out::println);
    }

    @Test
    public void test04() {
        String words = "Beijing welcome you I love China";
        Stream.of(words.split(" "))    // 当前流中的元素为各个单词
                .flatMap(word -> word.chars().boxed())
                .forEach(ch -> System.out.print((char)(ch.intValue())));
    }

    @Test
    public void test05() {
        String words = "Beijing welcome you I love China";
        Stream.of(words.split(" "))    // 当前流中的元素为各个单词
                // 最终形成的流中的元素为各个单词的字母
                .flatMap(word -> Stream.of(word.split("")))
                .forEach(System.out::print);
    }

3 flatMap (Function )

这是一个无状态操作。其功能是将流中的元素映射为多个值(流),即扁平化 map。其适用场景为流中原来的每个元素为集合,该方法用于将每个集合元素全部打散,然后添加到流中。

由于其参数为 Function,有输入与输出,所以 flatMap()操作会对流中元素产生影响。需要注意的是,该 Function 的输出类型为 Stream。
下面的例子要实现的功能为:输出指定字符串中的所有字母。

    @Test
    public void test07() {
        String words = "Beijing welcome you I love China";
        Stream.of(words.split(" "))    // 当前流中的元素为各个单词
                .flatMap(word -> Stream.of(word.split("")))
                .distinct()   // 去除重复的字母
                .forEach(System.out::print); //BeijngwlcomyuIvCha
    }

4 filter(Predicate action)

用于过滤掉不适合指定条件的流中的元素。其参数为 Predicate 断言,用于设置过滤条件。

    @Test
    public void test06() {
        String words = "Beijing welcome you I love China";
        Stream.of(words.split(" "))    // 当前流中的元素为各个单词
                // 当过滤条件为true时,当前元素会保留在流中,否则从流中删除
               .filter(word -> word.length() > 4)
               .forEach(System.out::println);
    }

5 peek(Consumer<? super T> action)

peek 功能同forEach, Consumer无返回值不改变流中元素,forEach是终结操作,peek不是。

2 有状态操作

1 distinct()

过滤掉流中重复元素,无参数。

2 sorted() 或 sorted(Comparator c)

对流中的元素进行排序。没有参数的 sorted()默认是按照字典序排序的,即按照 ASCII排序的。可以使用带参方法指定排序规则。

3 skip(long)

从流中去除指定个数的元素。从前面开始数

5 Stream 流的终止操作

终止操作的结束意味着 Stream 流操作结束。根据操作是否需要遍历流中的所有元素,可以将终止操作划分为短路操作与非短路操作。

1 非短路操作

1 forEach,forEachOrdered(Consumer action)

  • 对于流的并行操作,forEach()处理结果是无序的,
  • 对于流的并行操作,forEachOrdered()处理结果是有序的
    // 对于流的并行操作,forEach()处理结果是无序的
    @Test
    public void test01() {
        String words = "Beijing is the capital of China";
        Stream.of(words.split(" "))
                .parallel()
                .forEach(System.out::println);
    }

    // 对于流的并行操作,forEachOrdered()处理结果是有序的
    @Test
    public void test02() {
        String words = "Beijing is the capital of China";
        Stream.of(words.split(" "))
                .parallel()
                .forEachOrdered(System.out::println);
    }

2 collect(Collector col)

将流中的最终数据收集到集合中。其参数为 Collector 收集器,通过 Collectors 的静态方法 toList()、toSet()等可以获取到 Collector 对象。

    @Test
    public void test03() {
        String words = "Beijing is the capital of China";
        List<String> list = Stream.of(words.split(" "))
                .collect(Collectors.toList());
        System.out.println(list);
    }

3 toArray()

将流中的最终数据收集到数组中。

4 count()

统计流中的元素个数。

5 reduce(BinaryOperator bo)

reduce,减少,缩减。该方法的作用是将集合流最终转换为一个指定类型的数据。其参数为二元接口 BinaryOperator,即两个输入一个输出,且类型相同。由两个输入最终变为了一个输出,就达到了缩减 reduce 的效果了。

    // 计算流中所有单词的长度之和
    @Test
    public void test06() {
        String words = "Beijing is the capital of China";
        Optional<Integer> reduce = Stream.of(words.split(" "))
                .map(word -> word.length())
                .reduce((s1, s2) -> s1 + s2);
        // Optional的get()方法在其封装的对象为空时会抛出异常
        System.out.println(reduce.get());  // 结果为26
        // 当其封装的对象为空时会返回参数指定的值
        System.out.println(reduce.orElse(-1));
    }

6 max(Comparator com)

7 min(Comparator com)

2 短路操作

1 allMatch (predicate p)

用于判断是否所有元素都符合指定的条件,只要有一个不符合,马上结束匹配并返回false。只有所有都匹配上了才会返回 true。

2 anyMatch (predicate p)

用于判断是否存在符合条件的元素,只要找到一个符合的元素,马上结束匹配并返回true。

3 noneMatch(predicate p)

用于判断是否全部不符合条件,只要找到一个符合的,马上返回 false。只有所有都不符合才会返回 true。

4 findFirst()与 findAny()

findFirst()与 findAny()方法均会将从流中查找到的元素封装到 Optional 容器对象中。若没有找到,则 Optional 容器对象中的值会为空,Optional对象的 isPresent()方法返回值会为 false。

6 收集器

Stream 的 collect()方法可以将流中的最终数据收集到集合中,其为一个非短路终止操作。
其参数为 Collector 收集器,一般都是通过调用 Collectors 的静态方法获取收集器对象的。不过,收集器的功能远比将数据收集到集合要强大的多,还可以对收集的数据进行统计、分组等操作。
根据不同的需求调用 Collectors 的不同静态方法,获取到不同的 Collector 收集器。

1 转集合

toList()

toSet()

toCollection(supplier sup)

收集器默认使用的是无序的 HashSet,若要指定使用有序的 TreeSet,则可通过toCollection()方法指定。


@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String name;
    private String school;
    private String gender;
    private int age;
    private double score;
}

    // 获取所有参赛院校名单
    @Test
    public void test03() {
        Set<String> schools = students.stream()
                .map(Student::getSchool)
                .collect(Collectors.toCollection(TreeSet::new));
        System.out.println(schools);
    }

2 分组

  • 分组是指,按照流中指定的元素的属性值进行分组。
  • 统计是对元素个数的统计,所以无需指定属性用于统计。
    // 获取各个学校的学生(按照学校进行分组)
    @Test
    public void test04() {
        Map<String, List<Student>> schoolGroup = students.stream()
                .collect(Collectors.groupingBy(Student::getSchool));

        // 显示格式可读性很差
         System.out.println(schoolGroup);

        // 使用工具类显示map中的key-value
        MapUtils.verbosePrint(System.out, "学校", schoolGroup);

        // 获取key为“清华大学”的value,即获取所有清华大学的选手
        System.out.println(schoolGroup.get("清华大学"));
    }

    // 统计各个学校参赛选手人数
    @Test
    public void test05() {
        Map<String, Long> schoolCount = students.stream()
                .collect(Collectors.groupingBy(Student::getSchool,
                                               Collectors.counting())
                        );

        System.out.println(schoolCount);

        // 获取清华大学选手的人数
        System.out.println(schoolCount.get("清华大学"));
    }

    // 按照性别对所有参赛选手分组
    @Test
    public void test06() {
        Map<String, List<Student>> genderGroup = students.stream()
                .collect(Collectors.groupingBy(Student::getGender));

        MapUtils.verbosePrint(System.out, "性别", genderGroup);

        // 获取所有男生
        System.out.println(genderGroup.get("男"));
    }

3 布尔分块

布尔分块是按照指定断言的 true 与 false 结果进行分组,其只会划分为两组,且 key 只能是 true 或 false。
分块后,对两块内容按照指定的属性求平均值。

    // 按照性别对所有参赛选手分组
    @Test
    public void test07() {
        Map<Boolean, List<Student>> genderGroup = students.stream()
                .collect( Collectors.partitioningBy(s -> "男".equals(s.getGender())) );

        MapUtils.verbosePrint(System.out, "性别", genderGroup);

        // 获取所有男生
        System.out.println(genderGroup.get(true));
    }

    // 以95为标准按照成绩将所有参赛选手分组,分为大于95的组及不大于95的组
    @Test
    public void test08() {
        Map<Boolean, List<Student>> genderGroup = students.stream()
                .collect(Collectors.partitioningBy(s -> s.getScore() > 95));

        MapUtils.verbosePrint(System.out, "95划分成绩", genderGroup);

        // 获取所有成绩大于95的学生
        System.out.println(genderGroup.get(true));
    }

    // 以95为标准按照成绩将所有参赛选手分组,分为大于95的组及不大于95的组
    // 对这两组分别计算其平均分
    @Test
    public void test09() {
        Map<Boolean, Double> scoreGroupAvg = students.stream()
                .collect(Collectors.partitioningBy(s -> s.getScore() > 95,
                                                   Collectors.averagingDouble(Student::getScore))
                        );

        System.out.println(scoreGroupAvg);

        // 获取所有成绩大于95的所有学生的平均分
        System.out.println(scoreGroupAvg.get(true));
    }

4 获取汇总统计数据

汇总统计数据指的是对流中某一数据汇总后所统计出的最大值、最小值、平均值等数据。该方法只能适用于数值型数据统计。

    // 获取成绩相关的统计数据
    @Test
    public void test10() {
        DoubleSummaryStatistics scoreSummary = students.stream()
                .collect(Collectors.summarizingDouble(Student::getScore));

        System.out.println(scoreSummary);
        // 输出成绩的数量
        System.out.println("成绩个数:" + scoreSummary.getCount());
        // 输出成绩中的最小值
        System.out.println("最小成绩:" + scoreSummary.getMax());

    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值