|
|
@@ -0,0 +1,1019 @@
|
|
|
+# 20260804 课堂笔记 — 函数式编程(Functional Programming)+ Stream 流(中间操作 filter/limit/skip/map/concat/distinct + 终结操作 forEach/count + 收集操作 collect/Collectors:toList/toSet/toMap + 方法引用四种形式)+ 20260803 课后作业回顾(Lambda 应用 / Employee 员工排序)
|
|
|
+
|
|
|
+- **日期**: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<String> list = new ArrayList<>();
|
|
|
+list.add("万震山");
|
|
|
+list.add("万梓良");
|
|
|
+list.add("万事如意");
|
|
|
+list.add("王老五");
|
|
|
+list.add("万巨龙");
|
|
|
+list.add("张三");
|
|
|
+
|
|
|
+// - 把集合中所有以"万"开头的元素存储到一个新的集合
|
|
|
+ArrayList<String> list02 = new ArrayList<>();
|
|
|
+for (String s : list) {
|
|
|
+ if (s.startsWith("万")) {
|
|
|
+ list02.add(s);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// - 把"万"开头的集合中的长度为3的元素存储到一个新的集合
|
|
|
+ArrayList<String> 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<String> 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<T> filter(Predicate<? super T> predicate) 用于对流的数据进行过滤
|
|
|
+ Stream<T> limit(long maxSize) 返回这个流中元素组成的流,截取前指定参数个数的数据。
|
|
|
+ Stream<T> skip(long n) 跳过指定参数个数的数据,返回由该流剩余元素组成的流
|
|
|
+ public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) 合并a和b两个流为一个流
|
|
|
+ Stream<T> distinct() 返回由该流的不同元素组成的流,根据Object.equals(obj)得到是否相同。
|
|
|
+*/
|
|
|
+```
|
|
|
+
|
|
|
+### 常用中间方法速查
|
|
|
+
|
|
|
+| 方法 | 作用 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `Stream<T> filter(Predicate<? super T> predicate)` | **过滤** | 传入断言条件,返回满足条件的元素组成的新流 |
|
|
|
+| `Stream<T> limit(long maxSize)` | **截取** | 返回由该流中**前 `maxSize` 个**元素组成的流 |
|
|
|
+| `Stream<T> skip(long n)` | **跳过** | 跳过前 `n` 个元素,返回由该流**剩余元素**组成的流 |
|
|
|
+| `Stream<R> map(Function<? super T, ? extends R> mapper)` | **映射** | 把流中**每个元素**映射/转换成另一种类型,返回新类型组成的流 |
|
|
|
+| `Stream<T> concat(Stream a, Stream b)` | **合并** | 把 `a` 和 `b` **两个流合并为一个流**(静态方法) |
|
|
|
+| `Stream<T> distinct()` | **去重** | 返回由该流**不同元素**组成的流,根据 `Object.equals(obj)` 判断是否相同 |
|
|
|
+
|
|
|
+> `filter` 的参数类型 `Predicate<? super T>` 是 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<String> arrayStream = Arrays.stream(new String[]{"郑伊健", "陈浩南", "齐夏", "谢文东"});
|
|
|
+Stream<String> stringStream = Stream.of("长贵", "赵四", "刘能", "旺财");
|
|
|
+
|
|
|
+// ① filter / limit / skip 链式调用(Predicate 匿名内部类 vs Lambda 对比在注释中演示,此处先注释,思路已明确)
|
|
|
+// Stream<String> newStream = stringStream.filter(s -> s.startsWith("万"));
|
|
|
+// Stream<String> newNewStream = newStream.limit(2);
|
|
|
+// newNewStream.skip(1).forEach(System.out::println);
|
|
|
+
|
|
|
+// ② concat:合并 a 和 b 两个流为一个流(Stream 接口的静态方法)
|
|
|
+Stream<String> concat = Stream.concat(arrayStream, stringStream);
|
|
|
+concat.forEach(System.out::println);
|
|
|
+
|
|
|
+// ③ distinct:去重(依据 Object.equals),重复元素只保留一个
|
|
|
+List<String> stringList = List.of("长贵", "赵四", "郑伊健", "陈浩南", "齐夏", "刘能",
|
|
|
+ "郑伊健", "陈浩南", "齐夏", "郑伊健", "陈浩南", "齐夏");
|
|
|
+stringList.stream().distinct().forEach(System.out::println);
|
|
|
+```
|
|
|
+
|
|
|
+### map 映射示例(Demo02):把学生流映射成姓名流
|
|
|
+
|
|
|
+**map**(映射)是流中非常常用的中间方法:把流中的**每个元素**转换成另一种类型/形式,返回新类型的流。典型场景——学生集合中**提取所有学生的姓名**生成一个新集合。
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo02.java(main 方法)
|
|
|
+// 需求:把学生集合中每个学生的 name 提取出来生成一个新的集合
|
|
|
+// 传统写法:for 循环 + 中间集合
|
|
|
+ArrayList<String> nameList = new ArrayList<>();
|
|
|
+for (Student s : list) {
|
|
|
+ nameList.add(s.getName());
|
|
|
+}
|
|
|
+
|
|
|
+// Stream 写法:map 把"学生"映射成"姓名",再收集成 List
|
|
|
+List<String> nameList2 = list // 集合对象
|
|
|
+ .stream() // 获取流
|
|
|
+ .map(student -> student.getName()) // 映射:获取每个学生对象的姓名
|
|
|
+ .collect(Collectors.toList()); // 收集成新集合
|
|
|
+```
|
|
|
+
|
|
|
+> map 的参数是 JDK 内置函数式接口 `Function<T, R>`(有参有返回值)——`student -> student.getName()` 就是 `Function<Student, String>` 的简化写法。
|
|
|
+
|
|
|
+### 链式调用示意(流水线视角)
|
|
|
+
|
|
|
+```
|
|
|
+Stream.of("万震山", "万梓良", "万事如意", "王老五")
|
|
|
+ .filter(姓"万") // 中间方法 → 剩 "万震山"、"万梓良"、"万事如意"
|
|
|
+ .limit(2) // 中间方法 → 截取前 2 个 → "万震山"、"万梓良"
|
|
|
+ .skip(1) // 中间方法 → 跳过前 1 个 → "万梓良"
|
|
|
+ .forEach(...) // 终结方法 → 只输出 "万梓良"
|
|
|
+```
|
|
|
+
|
|
|
+### 关键理解
|
|
|
+
|
|
|
+1. **中间方法返回新流、可无限链式**:每调用一个中间方法都返回**新的 `Stream` 对象**,所以可以 `.filter().limit().skip()` 一直写下去。
|
|
|
+2. **`filter` 的 Lambda 本质**:`s -> s.startsWith("万")` 就是 `Predicate<String>` 匿名内部类 `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<String> 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<String>() {
|
|
|
+ // 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<? super T> action); 对此流的每个元素执行操作(不一定是输出)
|
|
|
+ long count(); 返回此流中的元素数
|
|
|
+*/
|
|
|
+```
|
|
|
+
|
|
|
+### 常用终结方法速查
|
|
|
+
|
|
|
+| 方法 | 作用 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `void forEach(Consumer<? super T> action)` | **遍历** | 对流的每个元素执行操作(不一定是输出,也可以做其他事) |
|
|
|
+| `long count()` | **计数** | 返回此流中的元素个数(注意返回值是 `long`) |
|
|
|
+
|
|
|
+> `forEach` 的参数 `Consumer<? super T>` 是 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, A> R collect(Collector<? super T, A, R> collector); 把结果收集到集合中
|
|
|
+ Object[] toArray(); 把结果收集到数组,生成一个新的结果集数组
|
|
|
+ default List<T> 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<String> stringList = List.of("长贵", "赵四", "郑伊健", "陈浩南", "齐夏", "刘能",
|
|
|
+ "郑伊健", "陈浩南", "齐夏", "郑伊健", "陈浩南", "齐夏");
|
|
|
+
|
|
|
+// ① 去重后收集成 List 集合
|
|
|
+List<String> list1 = stringList.stream() // 集合对象
|
|
|
+ .distinct() // 去重
|
|
|
+ .collect(Collectors.toList()); // 收集成 List
|
|
|
+System.out.println(list1);
|
|
|
+
|
|
|
+// ② 过滤"伊"后收集成 Set 集合(Set 天然去重,即使不过滤 distinct 也只有不重复的)
|
|
|
+Set<String> 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<Student> list = new ArrayList<>(List.of(s1, s2, s3, s4, s5, s6, s7));
|
|
|
+
|
|
|
+/*
|
|
|
+ Collectors.toMap 的完整签名:
|
|
|
+ public static <T, K, U> Collector<T, ?, Map<K, U>> toMap(
|
|
|
+ Function<? super T, ? extends K> keyMapper, // 取"键"的函数
|
|
|
+ Function<? super T, ? extends U> valueMapper // 取"值"的函数
|
|
|
+ )
|
|
|
+*/
|
|
|
+
|
|
|
+// 方式一:Lambda 写法 —— toMap(键, 值)
|
|
|
+Map<Integer, Student> map = list // 集合对象
|
|
|
+ .stream() // 生成流
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ student -> student.getId(), // 键:学号 id
|
|
|
+ student -> student // 值:学生对象本身
|
|
|
+ ));
|
|
|
+System.out.println(map);
|
|
|
+
|
|
|
+// 方式二:匿名内部类写法(Function 函数式接口,与 Lambda 等价)
|
|
|
+Map<Integer, Student> map1 = list.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ new Function<Student, Integer>() { // keyMapper
|
|
|
+ @Override
|
|
|
+ public Integer apply(Student student) {
|
|
|
+ return student.getId();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ new Function<Student, Student>() { // valueMapper
|
|
|
+ @Override
|
|
|
+ public Student apply(Student student) {
|
|
|
+ return student;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ));
|
|
|
+```
|
|
|
+
|
|
|
+### 关键理解
|
|
|
+
|
|
|
+1. **`collect` 是终结操作**:调用完流就结束,但结果不是直接输出,而是**收集到集合/数组**,方便后续继续使用。
|
|
|
+2. **`Collectors` 是收集工具类**:类比 `Arrays`(数组工具类)、`Collections`(集合工具类),`Collectors` 专门为流提供收集器——`toList()` / `toSet()` / `toMap()`。
|
|
|
+3. **`toMap` 的两个参数都是 `Function`**:`Function<T, R>` 是 JDK 内置函数式接口(有参有返回值)——第一个函数从元素中取**键**(keyMapper),第二个函数从元素中取**值**(valueMapper)。`student -> student.getId()` 取学号当键,`student -> student` 取学生对象当值。
|
|
|
+4. **Lambda vs 匿名内部类**:`student -> student.getId()` 就是 `new Function<Student, Integer>(){...}` 的简化写法,两种方式等价,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<E> 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<String> list = new ArrayList<>();
|
|
|
+ Stream<String> stream = list.stream();
|
|
|
+
|
|
|
+ // 2. Map体系集合:把 Map 转换成 Set,间接的生成流
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
+ Stream<String> keyStream = map.keySet().stream(); // key 就是 Set 集合
|
|
|
+ Stream<Object> valueStream = map.values().stream(); // value 就是 List 集合
|
|
|
+
|
|
|
+ // 3. 数组:通过 Arrays 中的静态方法 stream 生成流
|
|
|
+ String[] array = {"郑伊健", "陈浩南", "齐夏", "谢文东"};
|
|
|
+ Stream<String> arrayStream = Arrays.stream(array);
|
|
|
+
|
|
|
+ // 4. 相同类型的多个数据:通过 Stream 接口的静态方法 of(T...value) 生成流
|
|
|
+ Stream<String> stringStream = Stream.of("长贵", "赵四", "刘能", "旺财");
|
|
|
+ Stream<Integer> 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<Employee>`(自然排序),添加 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<Employee> {
|
|
|
+ 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<Employee>(自然排序:按薪资降序,薪资相同按工号;工号重复自动去重)
|
|
|
+ TreeSet<Employee> 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<Employee> 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<Employee>() { 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<String>`,存入"万震山、万梓良、万事如意、王老五、万巨龙、张三"
|
|
|
+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<Employee>`,`compareTo` 按薪资降序、薪资相同按工号
|
|
|
+2. `equals` / `hashCode` 以工号判定(与 compareTo 口径一致)
|
|
|
+3. `TreeSet<Employee>` 存入 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<String>` 函数式接口 `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<T>` 是有参无返回值的函数式接口,可以做任意动作(打印、计数、写入等)
|
|
|
+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<Integer, Student>`,键是学号、值是学生对象
|
|
|
+5. 尝试把 `s -> s.getId()` 改写成匿名内部类 `new Function<Student, Integer>() {...}`,对比两种写法
|
|
|
+
|
|
|
+**思考**:
|
|
|
+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<Student, Integer>(){...}` 是什么关系?—— 等价,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<Integer> 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<Integer> 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 中方法引用大量应用 |
|