# 20260804 课堂笔记 — 函数式编程与 Stream 流 - **日期**:2026-08-04 - **项目**:`c260804` - **包路径**:`course` / `homework0803` - **作者**:WanJL --- ## 目录 1. [函数式编程思想:概念与特点](#1-函数式编程思想概念与特点) 2. [Lambda 表达式回顾(JDK8 语法糖)](#2-lambda-表达式回顾jdk8-语法糖) 3. [Stream 流入门案例:筛选"万"开头 + 长度为 3](#3-stream-流入门案例筛选万开头--长度为-3) 4. [Stream 流的思想:三类方法(获取 / 中间 / 终结)](#4-stream-流的思想三类方法获取--中间--终结) 5. [Stream 流中间操作方法:filter / limit / skip / map / concat / distinct](#5-stream-流中间操作方法filter--limit--skip--map--concat--distinct) 6. [方法引用(Method Reference):四种形式](#6-方法引用method-reference四种形式) 7. [Stream 流终结操作方法:forEach / count](#7-stream-流终结操作方法foreach--count) 8. [Stream 流收集操作方法:collect + Collectors(toList / toSet / toMap)](#8-stream-流收集操作方法collect--collectorstolist--toset--tomap) 9. [生成 Stream 流的四种方式](#9-生成-stream-流的四种方式) 10. [20260803 课后作业回顾:Lambda 表达式应用(Addable / Flyable)](#10-20260803-课后作业回顾lambda-表达式应用addable--flyable) 11. [20260803 课后作业回顾:Employee 员工管理(TreeSet 自然排序 + Comparator 按姓名 + 方法引用)](#11-20260803-课后作业回顾employee-员工管理treeset-自然排序--comparator-按姓名--方法引用) 12. [随堂练习要点](#12-随堂练习要点) 13. [拓展阅读](#13-拓展阅读) --- ## 1. 函数式编程思想:概念与特点 ### 概念 **函数式编程**是一种**编程范式**,核心思想就是把计算看作是数学中的**函数计算**——程序由一些**数学函数的组合**构成,通过组合这些函数来完成计算任务;而不是像命令式那样写一条一条的**语句序列**来达到目的。 ```java // 来源:course/Demo01.java(注释部分) /* 函数式编程是一种编程范式,核心思想就是把计算看作是数学中的函数计算, 在函数式编程的过程中,程序由一些数学函数的组合构成,通过组合这些函数来完成计算任务。 而不是像命令式的语句序列一样来达到目的。 */ ``` ### 函数式编程的五大特点 | 特点 | 说明 | |------|------| | **不可变性** | 变量只能被赋值一次,在其生命周期内保持不变,减少程序错误,更好维护 | | **高阶函数** | 支持把**函数作为参数传递**,以及**把函数作为返回值**,可创建更通用的函数 | | **函数组合** | 通过组合多个小的、独立的函数构建更大、更复杂的函数(核心思想) | | **无副作用** | 函数执行结果**只取决于输入的参数**,不会对函数外部环境产生影响 | | **延迟求值** | 最终值只在**必要的时候**才被计算,提高程序执行效率 | ### 应用场景 > 一般用在**并发编程、异步编程、数据处理**以及各种**机器学习和数据科学**等方向中。 ### Java 中的体现 Java 中使用 **Lambda 表达式**就是一种函数式编程的体现,**Lambda 属于 JDK8 的一种语法糖**: ```java // 来源:course/Demo01.java(注释部分) (参数) -> {代码块} // 这就是 Java 语法糖 ``` ### 关键理解 1. **函数式编程 ≠ 新语言**:Java 通过 Lambda 表达式把函数式编程"引进来",它是对**匿名内部类**的语法简化。 2. **与面向对象的对比**:面向对象强调"通过对象的形式做事情",函数式编程强调"**做什么**",忽略繁琐的"以什么形式去做"。 3. **`Stream 流`才是真正把函数式编程引入 Java**(详见第 3~9 节)。 --- ## 2. Lambda 表达式回顾(JDK8 语法糖) 昨天(0803)已经学习了 Lambda 的**标准格式、省略模式、使用前提**,今天是它的应用延续。 ### 标准格式回顾 ```java (形式参数) -> {代码块} ``` **三要素**:形式参数、箭头 `->`、代码块。 ### 使用前提(回顾) 1. **必须要有接口**——Lambda 的本质是简化接口的匿名实现 2. **接口中有且只有一个抽象方法**——即必须是**函数式接口** ### 与匿名内部类的区别(回顾) | 对比维度 | 匿名内部类 | Lambda 表达式 | |----------|-----------|---------------| | 需要的类型 | 接口、抽象类、普通具体类均可 | **只能是接口** | | 使用限制 | 多抽象方法也能用 | **必须只有一个抽象方法** | | 实现原理 | 编译后产生独立 `.class` 文件 | 编译后**不产生单独 `.class` 文件**,运行期动态生成字节码 | --- ## 3. Stream 流入门案例:筛选"万"开头 + 长度为 3 ### 案例需求 按照下面的要求完成集合的创建和遍历: 1. 创建一个集合,存储多个字符串元素 2. 把集合中所有以 **"万"开头** 的元素存储到一个新的集合 3. 把"万"开头的集合中**长度为 3** 的元素存储到一个新的集合 4. 遍历上一步得到的集合 ### 传统写法(以前学过的写法) ```java // 来源:course/Demo01.java(注释部分:传统 for-each 写法) ArrayList list = new ArrayList<>(); list.add("万震山"); list.add("万梓良"); list.add("万事如意"); list.add("王老五"); list.add("万巨龙"); list.add("张三"); // - 把集合中所有以"万"开头的元素存储到一个新的集合 ArrayList list02 = new ArrayList<>(); for (String s : list) { if (s.startsWith("万")) { list02.add(s); } } // - 把"万"开头的集合中的长度为3的元素存储到一个新的集合 ArrayList list03 = new ArrayList<>(); for (String s : list) { if (s.startsWith("万") && s.length() == 3) { list03.add(s); } } // - 遍历上一步得到的集合 System.out.println(list03); ``` **缺点**:要写**好几个循环** + **好几个中间集合**,代码繁琐、语义不直观。 ### Stream 流写法 ```java // 来源:course/Demo01.java(注释部分:Stream 写法) ArrayList list = new ArrayList<>(List.of("万震山", "万梓良", "万事如意", "王老五", "万巨龙", "张三")); // Stream流 - 把集合中所有以"万"开头的元素存储到一个新的集合 list.stream().filter(s -> s.startsWith("万")).forEach(System.out::println); // Stream流 - 把"万"开头的集合中的长度为3的元素存储到一个新的集合 list.stream() .filter(s -> s.startsWith("万")) // 过滤"万"开头 .filter(s -> s.length() == 3) // 过滤长度为 3 .forEach(System.out::println); // 遍历输出 ``` ### Stream 流的好处 **可以直接阅读代码的字面意思**,就能展示跟逻辑无关的代码语义: ``` list ---> 集合对象 .stream() ---> 获取流 .filter(s -> s.startsWith("万")) ---> 过滤(过滤方式是找"万"开头) .forEach(System.out::println); ---> foreach 遍历(输出) ``` > 不需要再创建一个个中间集合,**一行一步操作**,链式调用非常直观。 ### 关键理解 1. **`filter` 是过滤(筛选)操作**:传入一个判断条件(Lambda),返回符合条件的新流。 2. **`forEach` 是遍历操作**:对流中的每个元素执行传入的动作(如打印)。 3. **只关注"做什么"**:不用写循环变量、不用管中间集合的增删——这正是函数式编程的体现。 4. **`Stream 流才是真正把函数式编程引入到 Java 中`**。 --- ## 4. Stream 流的思想:三类方法(获取 / 中间 / 终结) ### 概念 **Stream 流**:你可以把它理解为一条**流水线**、水流——数据从一头进,经过中间一道道工序(过滤、去重、跳过……),最后从另一头产出结果。 ```java // 来源:course/Demo01.java(注释部分) /* Stream流的思想: Stream流总体来说,分为三类方法: 1、获取方法 -- 获取Stream流 |- 创建一条流水线,并且把数据放在流水线上准备进行操作。 2、中间方法 -- 处理Stream流 |- 流水线上的操作,一次操作完成后,还可以继续进行其他操作 3、终结方法 -- 执行最后的操作 |- 一个Stream流水线只能有一个终结方法,是流水线上的最后一个操作。 */ ``` ### 三类方法对比 | 类别 | 作用 | 特点 | |------|------|------| | **获取方法** | 创建一条流水线,把数据放上去准备操作 | 一切的**起点** | | **中间方法** | 流水线上的操作(过滤、去重、跳过、截断、映射…) | 执行完**还可以继续链式调用**,如 `.filter().filter()` | | **终结方法** | 流水线上**最后一个**操作(遍历、计数、收集、最值…) | 一条流水线**只能有一个**终结方法,调用后流结束 | ### 关键理解 1. **中间方法可以连续调用多个**:如 `stream.filter(...).filter(...)`——每调用一次仍在"流水线上"。 2. **终结方法只能有一个**:它宣告流水线的终点,调用完这条流就结束了。 3. 常用中间方法(`filter`/`limit`/`skip`/`map`/`concat`/`distinct`)详见**第 5 节**;方法引用详见**第 6 节**;终结方法 `forEach`/`count` 详见**第 7 节**;收集方法 `collect` + `Collectors` 详见**第 8 节**。 --- ## 5. Stream 流中间操作方法:filter / limit / skip / map / concat / distinct ### 概念 **中间操作方法**的意思是:执行完此方法后,**Stream 流依然可以继续执行其他操作**——中间方法往往会**返回一个新的流对象**(`Stream` 对象),就是为了让我们可以继续进行下一步操作(链式调用)。 ```java // 来源:course/Demo01.java(注释部分:中间操作方法定义) /* Stream流中间操作方法: 中间操作的意思是执行完此方法后,Stream流依然可以继续执行其他操作。 常用的方法: Stream filter(Predicate predicate) 用于对流的数据进行过滤 Stream limit(long maxSize) 返回这个流中元素组成的流,截取前指定参数个数的数据。 Stream skip(long n) 跳过指定参数个数的数据,返回由该流剩余元素组成的流 public static Stream concat(Stream a, Stream b) 合并a和b两个流为一个流 Stream distinct() 返回由该流的不同元素组成的流,根据Object.equals(obj)得到是否相同。 */ ``` ### 常用中间方法速查 | 方法 | 作用 | 说明 | |------|------|------| | `Stream filter(Predicate predicate)` | **过滤** | 传入断言条件,返回满足条件的元素组成的新流 | | `Stream limit(long maxSize)` | **截取** | 返回由该流中**前 `maxSize` 个**元素组成的流 | | `Stream skip(long n)` | **跳过** | 跳过前 `n` 个元素,返回由该流**剩余元素**组成的流 | | `Stream map(Function mapper)` | **映射** | 把流中**每个元素**映射/转换成另一种类型,返回新类型组成的流 | | `Stream concat(Stream a, Stream b)` | **合并** | 把 `a` 和 `b` **两个流合并为一个流**(静态方法) | | `Stream distinct()` | **去重** | 返回由该流**不同元素**组成的流,根据 `Object.equals(obj)` 判断是否相同 | > `filter` 的参数类型 `Predicate` 是 JDK 内置**函数式接口**(`java.util.function.Predicate`,有参有返回值、返回 boolean 的断言),昨天 0803 笔记中已介绍过。 ### 代码示例(Demo01 的 main 方法) ```java // 来源:course/Demo01.java(main 方法:中间方法演示) import java.util.function.Predicate; // 新增 import:Predicate 函数式接口 import java.util.stream.Stream; // 承接上文生成的流(数据已换为新示例): Stream arrayStream = Arrays.stream(new String[]{"郑伊健", "陈浩南", "齐夏", "谢文东"}); Stream stringStream = Stream.of("长贵", "赵四", "刘能", "旺财"); // ① filter / limit / skip 链式调用(Predicate 匿名内部类 vs Lambda 对比在注释中演示,此处先注释,思路已明确) // Stream newStream = stringStream.filter(s -> s.startsWith("万")); // Stream newNewStream = newStream.limit(2); // newNewStream.skip(1).forEach(System.out::println); // ② concat:合并 a 和 b 两个流为一个流(Stream 接口的静态方法) Stream concat = Stream.concat(arrayStream, stringStream); concat.forEach(System.out::println); // ③ distinct:去重(依据 Object.equals),重复元素只保留一个 List stringList = List.of("长贵", "赵四", "郑伊健", "陈浩南", "齐夏", "刘能", "郑伊健", "陈浩南", "齐夏", "郑伊健", "陈浩南", "齐夏"); stringList.stream().distinct().forEach(System.out::println); ``` ### map 映射示例(Demo02):把学生流映射成姓名流 **map**(映射)是流中非常常用的中间方法:把流中的**每个元素**转换成另一种类型/形式,返回新类型的流。典型场景——学生集合中**提取所有学生的姓名**生成一个新集合。 ```java // 来源:course/Demo02.java(main 方法) // 需求:把学生集合中每个学生的 name 提取出来生成一个新的集合 // 传统写法:for 循环 + 中间集合 ArrayList nameList = new ArrayList<>(); for (Student s : list) { nameList.add(s.getName()); } // Stream 写法:map 把"学生"映射成"姓名",再收集成 List List nameList2 = list // 集合对象 .stream() // 获取流 .map(student -> student.getName()) // 映射:获取每个学生对象的姓名 .collect(Collectors.toList()); // 收集成新集合 ``` > map 的参数是 JDK 内置函数式接口 `Function`(有参有返回值)——`student -> student.getName()` 就是 `Function` 的简化写法。 ### 链式调用示意(流水线视角) ``` Stream.of("万震山", "万梓良", "万事如意", "王老五") .filter(姓"万") // 中间方法 → 剩 "万震山"、"万梓良"、"万事如意" .limit(2) // 中间方法 → 截取前 2 个 → "万震山"、"万梓良" .skip(1) // 中间方法 → 跳过前 1 个 → "万梓良" .forEach(...) // 终结方法 → 只输出 "万梓良" ``` ### 关键理解 1. **中间方法返回新流、可无限链式**:每调用一个中间方法都返回**新的 `Stream` 对象**,所以可以 `.filter().limit().skip()` 一直写下去。 2. **`filter` 的 Lambda 本质**:`s -> s.startsWith("万")` 就是 `Predicate` 匿名内部类 `test()` 方法的简化写法(`s` 是元素,返回 boolean)。 3. **`limit` 与 `skip` 常配合分页**:`limit(n)` 截取前 n 个;`skip(m)` 跳过前 m 个——两者组合可实现"跳过 m 条、取 n 条"的分页效果。 4. **`concat` 是静态方法**:它属于 `Stream` 接口的**静态方法**(不是流对象方法),用法是 `Stream.concat(流a, 流b)`,把两个流合并成一个。 5. **`distinct` 依赖 equals**:去重依据是 `Object.equals(obj)`——自定义对象要正确去重,必须重写 `equals`/`hashCode`(正好呼应前几天的 Object 类知识点)。 6. **`map` 是"转换"而不是"筛选"**:`filter` 是"留下符合条件的",`map` 是"把每个元素变成另一种东西"(如把学生变成姓名、把 Integer 变成 String)。 --- ## 6. 方法引用(Method Reference)—— 四种形式 ### 概念 **方法引用**是 **Lambda 表达式的进一步简化**:当 Lambda 的方法体**只有一行代码**,且这行代码只是**调用某个已有的方法**时,就可以直接用 `::`(双冒号)引用那个方法,把参数按顺序传入。 ```java // 来源:course/Demo02.java(注释部分:方法引用四种形式) /* 方法引用: 引用类的静态方法: 类名::静态方法名 String::valueOf 引用对象的实例方法: 对象名::方法名 sb::append 引用类的实例方法: 类名::方法名 String::substring 引用类的构造方法: 类名::new String::new 使用前提(四种通用):在重写方法的时候,方法体里面【只有一行代码】,并且这行代码是 调用【某个类的静态方法 / 某个对象的成员方法 / 某个类的成员方法 / 某个类的构造方法】, 并且我们要把重写的【抽象方法中的所有参数都按照顺序传入】到这个方法中。 */ ``` ### 四种形式速查 | 形式 | 写法 | 示例 | 说明 | |------|------|------|------| | **引用类的静态方法** | `类名::静态方法名` | `String::valueOf` | 调用某个类的**静态**方法 | | **引用对象的实例方法** | `对象名::方法名` | `sb::append` | 调用**某个对象**的成员方法(非 static) | | **引用类的实例方法** | `类名::方法名` | `String::substring` | 调用**某个类**的成员方法 | | **引用类的构造方法** | `类名::new` | `String::new` | 调用某个类的**构造方法** | > 四种形式的共同前提:方法体只有一行代码,且抽象方法的**所有参数按顺序**传入被引用的方法中。 ### 代码示例一:引用类的静态方法(String::valueOf) ```java // 来源:course/Demo02.java(main 方法:map 后把 Integer 转成 String) // 先 map 出每个学生的 id(Integer),再 map 用 String::valueOf 把整数转成字符串 List stringIdList = list.stream() .map(s -> s.getId()) // 映射出学号(Integer 流) .map(String::valueOf) // 静态方法引用:Integer → String(相当于 new String(valueOf(元素))) .collect(Collectors.toList()); System.out.println(stringIdList); // 输出:[1001, 1002, 1003, ...] ``` ### 代码示例二:引用对象的实例方法(sb::append) ```java // 来源:course/Demo02.java(main 方法:把每个学生姓名追加到 StringBuilder) StringBuilder sb = new StringBuilder(); list.stream() .map(student -> student.getName()) // 取出姓名流 // 简化前——匿名内部类 Consumer // .forEach(new Consumer() { // public void accept(String s) { sb.append(s); } // }) // 简化后——Lambda 表达式 // .forEach(s -> sb.append(s)); .forEach(sb::append); // 对象方法引用:对每个元素调用 sb.append(元素) System.out.println(sb); ``` ### 代码示例三:引用类的实例方法(String::substring —— MethodDemo 四种实现方式对比) ```java // 来源:course/MethodDemo.java // 自定义函数式接口(内部接口):截取字符串 public interface UseString { String use(String str, int start, int length); } public static String subName(String name, UseString us) { return us.use(name, 0, 1); // 截取第 0 位开始的 1 个字符(取姓氏) } public static void main(String[] args) { // 方式 1:传统实现类 UseStringImpl usi = new UseStringImpl(); String lastName = subName("张三", usi); // 张 // 方式 2:匿名内部类 String lastName02 = subName("李四", new UseString() { public String use(String str, int start, int length) { return str.substring(start, length); } }); // 方式 3:Lambda 表达式 String lastName03 = subName("李四", (str, start, length) -> str.substring(start, length)); // 方式 4:引用类的实例方法 —— 类名::方法名(str.substring(start,length) 完全匹配) String lastName04 = subName("王五", String::substring); } ``` **方式 4 为什么能成立**:重写的抽象方法 `use(String str, int start, int length)` 方法体只有一行 `str.substring(start, length)`——它调用的是**某个类(String)的成员方法**,且抽象方法的三个参数 `str, start, length` **按顺序**正好对应 `substring` 的参数,所以可以直接写成 `String::substring`。 ### 代码示例四:引用类的构造方法(String::new) ```java // 来源:course/Demo02.java(main 方法:把姓名创建成 String 对象) list.stream() .map(name -> name.getName()) // 取出姓名流 .map(String::new) // 构造方法引用:等价于 new String(name) .forEach(System.out::println); ``` ### 关键理解 1. **方法引用是 Lambda 的语法糖**:`String::valueOf` ≈ `s -> String.valueOf(s)`;`sb::append` ≈ `s -> sb.append(s)`;`String::new` ≈ `s -> new String(s)`。 2. **本质仍是函数式接口**:方法引用替换的仍是 Lambda,底层还是函数式接口的实现——`String::substring` 实现的是 `UseString` 接口的 `use()` 方法。 3. **使用前提是"一行代码 + 参数顺序对应"**:四种形式都必须满足"方法体只有一行代码、抽象方法的所有参数按顺序传入",否则不能使用方法引用。 4. **区分三种实例方法引用**:`对象名::方法名`(如 `sb::append`,针对某个具体对象)与 `类名::方法名`(如 `String::substring`,第一个参数隐式作为调用对象)要分清——`类名::方法名` 形式下,抽象方法的**第一个参数**就是调用成员方法的那个对象。 5. **与 0803 已学方法的衔接**:`Comparator.comparing(Employee::getName)` 就是"引用类的实例方法"的一种应用(`getName` 是 Employee 的成员方法)。 --- ## 7. Stream 流终结操作方法:forEach / count ### 概念 **终结操作方法**的意思:执行完这个方法后,**Stream 流就不能再执行其他操作**了——一条流水线**只能有一个终结方法**,它是流水线上的最后一个操作。 ```java // 来源:course/Demo01.java(注释部分:终结操作方法定义) /* Stream流终结操作方法: 终结操作的意思就是执行完这个方法后,Stream流就不能再执行其他操作了。 void forEach(Consumer action); 对此流的每个元素执行操作(不一定是输出) long count(); 返回此流中的元素数 */ ``` ### 常用终结方法速查 | 方法 | 作用 | 说明 | |------|------|------| | `void forEach(Consumer action)` | **遍历** | 对流的每个元素执行操作(不一定是输出,也可以做其他事) | | `long count()` | **计数** | 返回此流中的元素个数(注意返回值是 `long`) | > `forEach` 的参数 `Consumer` 是 JDK 内置**函数式接口**(`java.util.function.Consumer`,有参无返回值)——昨天 0803 笔记中介绍过。 ### 关键理解 1. **终结方法 = 流水线终点**:调用 `forEach`/`count` 后,这条流就结束了,不能再继续 `.filter()` 等操作。 2. **`forEach` 不一定只是输出**:它的 `Consumer` 参数可以执行任意动作(打印、计数、写入集合等),`System.out::println` 只是最常见的一种(方法引用)。 3. **`count` 返回 `long`**:元素个数用 `long` 表示,配合中间方法可统计"过滤后还剩多少个"。 --- ## 8. Stream 流收集操作方法:collect + Collectors(toList / toSet / toMap) ### 概念 **收集操作方法**:当我们使用流对一些数据操作完毕后,**可以把流中的数据收集到集合**(List / Set / Map / 数组)中——`collect` 也是终结操作的一种,但它把结果"存起来"而非直接输出。 ```java // 来源:course/Demo01.java(注释部分:收集操作方法定义) /* Stream流收集操作方法: 当我们使用流对一些数据操作完毕后,可以把流中的数据收集到集合中。 R collect(Collector collector); 把结果收集到集合中 Object[] toArray(); 把结果收集到数组,生成一个新的结果集数组 default List toList() 把结果收集到List集合,生成一个新的List集合,这里返回的List集合是不可变的集合。 工具类Collectors提供了具体的收集方式: toMap(Function keyMapper, Function valueMapper) 把元素收集到map集合 toList() 把元素收集到List集合 toSet() 把元素收集到Set集合 */ ``` ### 收集方式速查 | 方法 | 收集目标 | 说明 | |------|----------|------| | `collect(Collector collector)` | 集合 | 配合 `Collectors` 工具类的具体收集器使用 | | `toArray()` | 数组 | 把结果收集成新数组 | | `toList()` | List | 把结果收集成新 List——**返回的 List 是不可变集合**(不能增删改) | | `Collectors.toList()` | List | 工具类 Collectors 提供的收集方式 | | `Collectors.toSet()` | Set | 工具类 Collectors 提供的收集方式(Set 天然去重) | | `Collectors.toMap(keyMapper, valueMapper)` | Map | 工具类 Collectors 提供的收集方式,两个参数都是 `Function` 函数式接口 | ### 代码示例一:Demo01 —— collect 收集成 List / Set ```java // 来源:course/Demo01.java(main 方法:收集操作演示) import java.util.stream.Collectors; // 新增 import:Collectors 收集工具类 List stringList = List.of("长贵", "赵四", "郑伊健", "陈浩南", "齐夏", "刘能", "郑伊健", "陈浩南", "齐夏", "郑伊健", "陈浩南", "齐夏"); // ① 去重后收集成 List 集合 List list1 = stringList.stream() // 集合对象 .distinct() // 去重 .collect(Collectors.toList()); // 收集成 List System.out.println(list1); // ② 过滤"伊"后收集成 Set 集合(Set 天然去重,即使不过滤 distinct 也只有不重复的) Set set = stringList.stream() // 集合对象 .filter(s -> s.contains("伊")) // 过滤包含"伊"的 .collect(Collectors.toSet()); // 收集成 Set System.out.println(set); ``` ### 代码示例二:ToMapDemo —— collect 收集成 Map(重点) 新增两个文件:`Student` 实体类 + `ToMapDemo` 演示类。 ```java // 来源:course/Student.java(实体类,作为 toMap 的值对象) public class Student { private Integer id; // 学号 private String name; // 姓名 // 无参构造 / 有参构造(id, name) // getter / setter // equals / hashCode:基于 id + name(Objects.equals / Objects.hash) // toString() } ``` ```java // 来源:course/ToMapDemo.java(main 方法) import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; // 创建 7 名学生放入 ArrayList Student s1 = new Student(1001, "项羽"); Student s2 = new Student(1002, "张良"); // ... s3~s7 略(项庄/刘邦/萧何/韩信/樊哙) ArrayList list = new ArrayList<>(List.of(s1, s2, s3, s4, s5, s6, s7)); /* Collectors.toMap 的完整签名: public static Collector> toMap( Function keyMapper, // 取"键"的函数 Function valueMapper // 取"值"的函数 ) */ // 方式一:Lambda 写法 —— toMap(键, 值) Map map = list // 集合对象 .stream() // 生成流 .collect(Collectors.toMap( student -> student.getId(), // 键:学号 id student -> student // 值:学生对象本身 )); System.out.println(map); // 方式二:匿名内部类写法(Function 函数式接口,与 Lambda 等价) Map map1 = list.stream() .collect(Collectors.toMap( new Function() { // keyMapper @Override public Integer apply(Student student) { return student.getId(); } }, new Function() { // valueMapper @Override public Student apply(Student student) { return student; } } )); ``` ### 关键理解 1. **`collect` 是终结操作**:调用完流就结束,但结果不是直接输出,而是**收集到集合/数组**,方便后续继续使用。 2. **`Collectors` 是收集工具类**:类比 `Arrays`(数组工具类)、`Collections`(集合工具类),`Collectors` 专门为流提供收集器——`toList()` / `toSet()` / `toMap()`。 3. **`toMap` 的两个参数都是 `Function`**:`Function` 是 JDK 内置函数式接口(有参有返回值)——第一个函数从元素中取**键**(keyMapper),第二个函数从元素中取**值**(valueMapper)。`student -> student.getId()` 取学号当键,`student -> student` 取学生对象当值。 4. **Lambda vs 匿名内部类**:`student -> student.getId()` 就是 `new Function(){...}` 的简化写法,两种方式等价,Lambda 更简洁(对应 0803 笔记的省略规则)。 5. **`toList()` 与 `Stream.toList()` 返回不可变集合**:收集出的 List 不能 `add`/`remove`(呼应 `List.of()` 不可变集合的坑)。 6. **`toSet()` 天然去重**:收集成 Set 本身就保证元素不重复。 7. **`toMap` 键冲突会抛异常**:如果多个元素映射出相同的键,`Collectors.toMap` 默认抛 `IllegalStateException`——所以键必须是唯一的(如学号 id)。 8. **贪心思想注释**:ToMapDemo 注释中提到的"贪心思想——局部最优解,不代表全局最优解",是程序设计中的一个思维比喻,提醒我们局部最优不等于全局最优。 --- ## 9. 生成 Stream 流的四种方式 ### 数据来源对应四种生成方式 | 数据来源 | 生成方式 | 说明 | |----------|----------|------| | **Collection 体系集合** | `集合对象.stream()` | 使用默认方法 `default Stream stream()` | | **Map 体系集合** | `map.keySet().stream()` / `map.values().stream()` | 把 Map 转换成 **Set 集合**,间接生成流 | | **数组** | `Arrays.stream(数组)` | `Arrays` 静态方法 `public static IntStream stream(int[] array)` | | **相同类型的多个数据** | `Stream.of(T... value)` | `Stream` 接口的静态方法 `of(T...value)` | ### 代码示例(Demo01 的 main 方法) ```java // 来源:course/Demo01.java package course; import java.util.*; import java.util.stream.IntStream; import java.util.stream.Stream; public class Demo01 { public static void main(String[] args) { // 1. Collection体系集合:使用默认方法 stream() 生成流 List list = new ArrayList<>(); Stream stream = list.stream(); // 2. Map体系集合:把 Map 转换成 Set,间接的生成流 Map map = new HashMap<>(); Stream keyStream = map.keySet().stream(); // key 就是 Set 集合 Stream valueStream = map.values().stream(); // value 就是 List 集合 // 3. 数组:通过 Arrays 中的静态方法 stream 生成流 String[] array = {"郑伊健", "陈浩南", "齐夏", "谢文东"}; Stream arrayStream = Arrays.stream(array); // 4. 相同类型的多个数据:通过 Stream 接口的静态方法 of(T...value) 生成流 Stream stringStream = Stream.of("长贵", "赵四", "刘能", "旺财"); Stream integerStream = Stream.of(10, 15, 20, 35, 46); } } ``` ### 关键理解 1. **`stream()` 是 Collection 接口的默认方法**:`List` / `Set` / `Queue` 这些 Collection 体系集合都能直接调用。 2. **Map 不能直接生成流**:因为 Map 不是 Collection 体系,需要先把 `keySet()`(Set)或 `values()`(Collection)取出来再 `.stream()`。 3. **`Arrays.stream()` 专门处理数组**:数组不是集合,不能调 `.stream()`,要用 `Arrays` 工具类的静态方法。 4. **`Stream.of(T... values)` 处理零散数据**:注意 `of` 的参数是**可变参数**(`T...`)——正好用上了昨天学的 Varargs!`Stream.of(10, 15, 20, 35, 46)` 本质就是 `Stream.of(new Integer[]{10,15,20,35,46})`。 --- ## 10. 20260803 课后作业回顾:Lambda 表达式应用(Addable / Flyable) ### 作业场景回顾 调用某个方法的时候,需要做哪些事情?核心思路: 1. **先看所属的类** 2. **再看修饰符**(访问权限修饰符、静态修饰符) - 如果是 `private` 修饰符 → 肯定调用不了 - 如果是 `static` 修饰 → 类名.方法名() 直接调用;否则创建对象用对象调用 3. **看方法的参数**:只要不是那 8 个基本数据类型,其他的都是**引用数据类型变量**,都需要传入**对象** - 传入的对象必须是该参数类型的,或者是该参数类型的**子类、实现类的对象**——这就形成了**多态** ```java // 来源:homework0803/p4_lambda/LambdaDemo.java(注释部分) // 只要不是那8个基本数据类型,其他的都是引用数据类型变量。都需要在调用方法的时候传入 【对象】 // 传入的对象必须是该参数类型的,或者是该参数类型的子类、实现类的对象。 这就形成了多态 ``` ### 函数式接口定义 ```java // 来源:homework0803/p4_lambda/Addable.java // 有参有返回值的函数式接口 package homework0803.p4_lambda; public interface Addable { int add(int x, int y); } ``` ```java // 来源:homework0803/p4_lambda/Flyable.java // 有参无返回值的函数式接口 package homework0803.p4_lambda; public interface Flyable { void fly(String s); } ``` ### Lambda 传参调用 ```java // 来源:homework0803/p4_lambda/LambdaDemo.java package homework0803.p4_lambda; public class LambdaDemo { public static void useAdd(Addable a) { System.out.println(a.add(10, 20)); } public static void useFlyable(Flyable f) { f.fly("今天天气不错...是风和日丽的..."); } public static void main(String[] args) { // 调用 useAdd 方法,传入参数有两种选择: // 1、自己新建一个实现类,实现接口,重写方法。 // 2、创建该接口的匿名内部类对象。 // 现在还有第 3 种:直接传入 Lambda 表达式(本质是简化后的匿名内部类) useAdd((x, y) -> x + y); // 有参有返回值:单语句 → 省略大括号、分号、return } } ``` ### 知识点总结 | 知识点 | 说明 | |--------|------| | **调用方法三步看** | ①所属类 ②修饰符(private 不能调 / static 类名调 / 否则对象调)③参数类型 | | **引用类型参数传对象** | 参数不是 8 大基本类型时,就要传**该类型或其子类/实现类的对象**(多态) | | **Lambda 是第三种传参方式** | 方法参数是函数式接口时,可传实现类对象、匿名内部类对象,或直接传 Lambda | | **`useAdd((x, y) -> x + y)`** | 两个参数(小括号不能省),方法体只有一条表达式语句(省大括号/分号/return) | --- ## 11. 20260803 课后作业回顾:Employee 员工管理(TreeSet 自然排序 + Comparator 按姓名 + 方法引用) ### 作业要求回顾 > 创建 `TreeSet`(自然排序),添加 5 名员工(其中**两名工号相同**,测试去重):按薪资降序、薪资相同再按工号;再用迭代器遍历输出。最后**通讯录按姓名排序**:重新创建 TreeSet 传入按姓名排序的 Comparator。 ### Employee 实体类:实现 Comparable + equals/hashCode ```java // 来源:homework0803/p5_employee/Employee.java package homework0803.p5_employee; import java.util.Objects; public class Employee implements Comparable { private String id; // 工号 private String name; // 姓名 private double salary; // 薪资 private String dept; // 部门 public Employee(String id, String name, double salary, String dept) { this.id = id; this.name = name; this.salary = salary; this.dept = dept; } // getter / setter ...(略) // 自然排序:按薪资降序,薪资相同再按工号 @Override public int compareTo(Employee o) { int result = Double.compare(o.getSalary(), this.salary); // 降序:交换参数 return result == 0 ? this.id.compareTo(o.getId()) : result; // 薪资相同按工号 } // equals / hashCode:以工号判定(与 compareTo 的"工号"判定口径一致) @Override public boolean equals(Object obj) { if (obj instanceof Employee e) { return Objects.equals(id, e.id); } return false; } @Override public int hashCode() { return Objects.hash(id); } } ``` ### EmployeeSystem:TreeSet 自然排序 + 迭代器遍历 ```java // 来源:homework0803/p5_employee/EmployeeSystem.java(部分) package homework0803.p5_employee; import java.util.Comparator; import java.util.Iterator; import java.util.TreeSet; public class EmployeeSystem { public static void main(String[] args) { // 1. 创建 TreeSet(自然排序:按薪资降序,薪资相同按工号;工号重复自动去重) TreeSet treeSet = new TreeSet<>(); treeSet.add(new Employee("E001", "张三", 12000, "技术部")); treeSet.add(new Employee("E002", "李四", 15000, "市场部")); treeSet.add(new Employee("E003", "王五", 15000, "技术部")); treeSet.add(new Employee("E004", "赵六", 9000, "财务部")); treeSet.add(new Employee("E005", "刘七", 12000, "市场部")); treeSet.add(new Employee("E002", "李四", 20000, "市场部")); // 工号重复 → 被去重 // 2. 迭代器遍历输出 → 观察:按薪资降序,薪资相同按工号;工号重复的自动去重 Iterator iterator = treeSet.iterator(); while (iterator.hasNext()) { Employee e = iterator.next(); System.out.println(e.getId() + "-" + e.getName() + "-" + e.getSalary() + "-" + e.getDept()); } // 3. 通讯录按姓名排序:重新创建 TreeSet,传入按姓名排序的 Comparator(此处用方法引用) treeSet = new TreeSet<>(Comparator.comparing(Employee::getName)); // 再添加 6 名员工(同上),迭代器遍历输出 → 按姓名排序 } } ``` ### 方法引用(Method Reference)—— 新知识 > 方法引用的**四种形式**(静态方法 / 对象实例方法 / 类实例方法 / 构造方法)详见**第 6 节**,本节是它在 Comparator 中的一次具体应用。 `Comparator.comparing(Employee::getName)` 里的 `Employee::getName` 就是**方法引用**——它是 Lambda 的一种**更简化的写法**: | 写法 | 代码 | 说明 | |------|------|------| | **匿名内部类** | `new Comparator() { compare(...) { return o1.getName().compareTo(o2.getName()); } }` | 最原始,最啰嗦 | | **Lambda 表达式** | `(o1, o2) -> o1.getName().compareTo(o2.getName())` | 省略类型、大括号、return | | **方法引用** | `Comparator.comparing(Employee::getName)` | 用 `类名::方法名` 直接引用现有方法,最简洁 | ```java // 来源:homework0803/p5_employee/EmployeeSystem.java(注释提示) // 可用匿名内部类,也可尝试用 Lambda:Comparator.comparing(Employee::getName) 或 // (o1, o2) -> o1.getName().compareTo(o2.getName()) ``` ### 知识点总结 | 知识点 | 说明 | |--------|------| | **自然排序 + 二次比较** | `compareTo` 先按薪资(`Double.compare`),薪资相同返回 0 时再按工号比较,保证唯一 | | **降序技巧** | 交换 `compare` 参数位置:`Double.compare(o.getSalary(), this.salary)` = 薪资降序 | | **compareTo / equals / hashCode 一致性** | 都以**工号**判定口径 → TreeSet 去重(E002 重复被丢弃)与排序行为一致,符合官方推荐 | | **`instanceof` 模式匹配** | `if (obj instanceof Employee e)` 直接得到强转后的变量 `e`(JDK16+ 特性) | | **`Comparator.comparing(类::getter)`** | 方法引用写法,`TreeSet<>(Comparator.comparing(Employee::getName))` = 按姓名排序 | | **方法引用 vs Lambda** | 方法引用是 Lambda 的进一步简化:`Employee::getName` ≈ `(e) -> e.getName()` | --- ## 12. 随堂练习要点 ### 练习一:Stream 流筛选集合元素 **核心步骤**: 1. 创建 `ArrayList`,存入"万震山、万梓良、万事如意、王老五、万巨龙、张三" 2. 用 `list.stream().filter(s -> s.startsWith("万")).forEach(...)` 筛选并遍历"万"开头 3. 再用 `.filter(s -> s.length() == 3)` 叠加筛选长度为 3 的元素 **思考**: 1. 传统 for 循环要写几个集合?Stream 流为什么不用中间集合?—— 传统写法每筛选一次要建一个新集合;Stream 用 `filter` 链式过滤,数据在流水线上流动,不需要中间容器 2. `.filter()` 属于三类方法中的哪一类?—— 中间方法(可连续调用多个);`.forEach()` 呢?—— 终结方法(只能有一个,流水线终点) ### 练习二:四种方式生成 Stream 流 **核心步骤**: 1. Collection 集合:`list.stream()` 2. Map 集合:`map.keySet().stream()` / `map.values().stream()` 3. 数组:`Arrays.stream(array)` 4. 零散数据:`Stream.of(10, 15, 20, 35, 46)` **思考**: 1. Map 为什么不能直接 `.stream()`?—— Map 不是 Collection 体系,要先 `keySet()` 或 `values()` 取出 Set/Collection 2. `Stream.of(T... values)` 用了昨天学的什么语法?—— 可变参数 Varargs(`T...`),所以可以传任意个数数据 ### 练习三:函数式接口 + Lambda 传参(作业一) **核心步骤**: 1. 定义有参有返回值的函数式接口 `Addable`(`int add(int x, int y)`) 2. 定义有参无返回值的函数式接口 `Flyable`(`void fly(String s)`) 3. 定义 `useAdd(Addable a)` / `useFlyable(Flyable f)` 方法,调用时直接传 Lambda 4. 用 `useAdd((x, y) -> x + y)` 体会单语句省略大括号/分号/return **思考**: 1. 方法参数是函数式接口时,能传哪几种东西?—— 实现类对象、匿名内部类对象、Lambda 表达式(三种都能体现多态) 2. 调用方法前"三步看"是什么?—— 看所属类 → 看修饰符(private/static)→ 看参数类型(引用类型要传对象) ### 练习四:Employee 员工排序(作业二) **核心步骤**: 1. `Employee` 实现 `Comparable`,`compareTo` 按薪资降序、薪资相同按工号 2. `equals` / `hashCode` 以工号判定(与 compareTo 口径一致) 3. `TreeSet` 存入 6 名员工(含工号重复的 E002),迭代器遍历观察**排序 + 去重** 4. 重新 `new TreeSet<>(Comparator.comparing(Employee::getName))` 按姓名排序,迭代器遍历 **思考**: 1. E001(12000)和 E005(12000)薪资相同为什么都能存入?—— compareTo 薪资相同后继续比较工号(E001 ≠ E005),返回值不为 0,不视为重复 2. 工号重复的 E002 为什么被去重?—— compareTo 判定重复(0)且 equals/hashCode 也按工号判定,TreeSet 去重规则生效 3. `Comparator.comparing(Employee::getName)` 中的 `Employee::getName` 是什么写法?—— 方法引用,等价于 Lambda `(e) -> e.getName()` 4. 方法引用比 Lambda 省了什么?—— 省掉参数列表和箭头,直接用 `类名::方法名` 引用现有方法(当前作业已用上) ### 练习五:Stream 流中间操作方法(filter / limit / skip / concat / distinct) **核心步骤**: 1. `Stream.of("万震山", "万梓良", "万事如意", "王老五")` 生成流 2. `.filter(s -> s.startsWith("万"))` 过滤出"万"开头 → 再 `.limit(2)` 截取前 2 个 → 再 `.skip(1)` 跳过前 1 个 → `.forEach(System.out::println)` 输出 3. 体会"每个中间方法都返回**新的流对象**",所以能一直链式调用 4. 尝试 `Stream.concat(流a, 流b)` 合并两个流;给包含重复元素的数据加 `.distinct()` 去重 **思考**: 1. 中间方法和终结方法怎么区分?—— 中间方法执行完返回**新的 Stream**,还能继续链式操作;终结方法执行完流就结束,不能再调用其他方法 2. `filter` 的 Lambda `s -> s.startsWith("万")` 本质是什么?—— 是 `Predicate` 函数式接口 `test()` 方法的简化(有参、返回 boolean 的断言) 3. `limit` 和 `skip` 组合能实现什么?—— 分页:`skip(m)` 跳过前 m 条、`limit(n)` 取 n 条 4. `distinct` 去重的依据是什么?—— `Object.equals(obj)`,自定义对象要重写 `equals`/`hashCode` 才能正确去重 ### 练习六:Stream 流终结操作方法(forEach / count) **核心步骤**: 1. `list.stream().forEach(System.out::println)` 遍历输出每个元素 2. `long count = list.stream().filter(s -> s.contains("伊")).count();` 统计过滤后元素个数 3. 尝试在 `forEach` 之后继续调用 `.filter(...)`,观察编译/运行结果 **思考**: 1. 为什么 `forEach` 之后不能再调用其他方法?—— `forEach` 是终结操作,调用后流就结束了,不能继续链式操作 2. `forEach` 的参数 `Consumer` 一定只能打印吗?—— 不一定,`Consumer` 是有参无返回值的函数式接口,可以做任意动作(打印、计数、写入等) 3. `count()` 的返回值类型是什么?—— `long`,不是 `int` ### 练习七:Stream 流收集操作(collect / Collectors:toList / toSet / toMap) **核心步骤**: 1. `list.stream().distinct().collect(Collectors.toList())` 去重后收集成 List,打印观察 2. `list.stream().filter(...).collect(Collectors.toSet())` 过滤后收集成 Set,观察 Set 天然去重 3. 定义 `Student` 实体类(id、name、getter/setter、equals/hashCode、toString),创建 7 名学生 4. `list.stream().collect(Collectors.toMap(s -> s.getId(), s -> s))` 收集成 `Map`,键是学号、值是学生对象 5. 尝试把 `s -> s.getId()` 改写成匿名内部类 `new Function() {...}`,对比两种写法 **思考**: 1. `collect` 是中间操作还是终结操作?—— 终结操作(执行完流结束),但结果不是直接输出,而是**收集到集合/数组**里 2. `Collectors.toMap` 的两个参数分别是什么?—— 第一个 `keyMapper` 从元素中取**键**,第二个 `valueMapper` 从元素中取**值**,都是 `Function` 函数式接口 3. 为什么用学号当键?—— 键必须唯一;如果两个元素映射出相同键,`toMap` 默认抛 `IllegalStateException` 4. `collect(Collectors.toList())` 得到的 List 能 `add` 吗?—— 不能,`Collectors.toList()` / `Stream.toList()` 返回的是**不可变集合**,直接增删改抛 `UnsupportedOperationException` 5. Lambda `s -> s.getId()` 与匿名内部类 `new Function(){...}` 是什么关系?—— 等价,Lambda 是后者的简化写法 ### 随堂练习参考解答(已提供代码) 对应今日随堂练习的参考解答已写入授课代码: | 练习 | 参考代码路径 | |------|-------------| | 练习 3 分页:limit/skip 分页 + `Stream.toList()` | [`exericse/exericse03/PageTest.java`](./../授课代码/c260804/src/exericse/exericse03/PageTest.java) | **PageTest 参考解答要点**: ```java // 来源:exericse/exericse03/PageTest.java // 1. limit(5):截取前 5 个 Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).limit(5).forEach(i -> System.out.print(i + " ")); // 2. skip(3):跳过前 3 个 Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).skip(3).forEach(i -> System.out.print(i + " ")); // 3. 用 Stream.toList() 把流收集成 List(注意:返回不可变集合) List list = Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).toList(); // 分页方法:skip((pageNo-1) * pageSize).limit(pageSize) public static void showPage(List list, int pageNo, int pageSize) { list.stream() .skip((pageNo - 1) * pageSize) // 跳过前面的页 .limit(pageSize) // 截取本页数量 .forEach(i -> System.out.print(i + " ")); } showPage(list, 1, 3); // 0 1 2 showPage(list, 2, 3); // 3 4 5 showPage(list, 3, 3); // 6 7 8 showPage(list, 4, 3); // 9 ``` --- ## 13. 拓展阅读 ### 官方文档 - [函数式编程概念(维基百科)](https://en.wikipedia.org/wiki/Functional_programming) - [Java 8 Lambda 表达式(Oracle 教程)](https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html) - [Java 8 方法引用(Oracle 教程)](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html) - [java.util.stream 包 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/package-summary.html) - [Stream 接口 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Stream.html) - [Stream.of() (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Stream.html#of(T...)) - [Arrays.stream() (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Arrays.html#stream(T[])) - [Comparator.comparing() (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Comparator.html#comparing(java.util.function.Function)) - [Collectors 工具类 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Collectors.html) - [Collector 接口 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Collector.html) - [Consumer 函数式接口 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/function/Consumer.html) - [Function 函数式接口 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/function/Function.html) - [Stream.map() (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Stream.html#map(java.util.function.Function)) - [Stream.toList() (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Stream.html#toList()) ### 推荐阅读 - 《Java 8 实战》第 4 章 引入流、第 5 章 使用流 —— Stream 流的入门与常用操作 - 《Java 8 实战》第 3 章 Lambda 表达式 —— 方法引用、复合 Lambda - 《Effective Java》第 3 版 第 42~44 条:Lambda 优先于匿名类、方法引用优先于 Lambda - 《Java 核心技术 卷 I》第 6 章 接口、lambda 表达式与内部类 - 《Java 8 实战》第 3 章 3.3 节 —— 方法引用:四种形式与使用场景 ### 相关知识点 | 前置知识 | 当前知识 | 后续知识 | |----------|----------|----------| | 匿名内部类(0727) | 函数式编程思想 | 方法引用、Stream 高阶函数 | | Lambda 表达式(0803) | Lambda 应用(Addable/Flyable) | Stream API 的 filter/map/collect | | Comparable/Comparator(0803) | TreeSet 员工排序 + 方法引用 | Stream 流的 sorted 排序、Collectors | | 可变参数 Varargs(0803) | `Stream.of(T... values)` 可变参数应用 | 方法引用 & 收集器 | | Collection 体系集合(0729) | 生成 Stream 流的四种方式 | 中间方法(filter/limit/skip/distinct/map) | | 函数式接口 Predicate(0803) | 中间操作方法(filter/limit/skip/concat/distinct) | Stream 映射 map、终结方法 collect/count、Collectors 收集器 | | 函数式接口 Consumer/Function(0803) | 终结操作 forEach/count、收集操作 collect/Collectors(toList/toSet/toMap) | Collectors 高级用法(groupingBy 分组、joining 拼接)、Stream 与集合互转 | | Lambda 表达式(0803) | 方法引用四种形式(静态/对象实例/类实例/构造) | 方法引用的高级组合、Stream 中方法引用大量应用 |