# 2026-08-04 Stream 流与函数式编程专项练习(课堂练习用) > **说明**:以下习题围绕今日授课核心内容——**函数式编程与 Stream 流** 展开,涵盖 Stream 流的四种生成方式(Collection.stream() / Map 转 Set / Arrays.stream() / Stream.of())、中间操作方法(filter / limit / skip / concat / distinct)、终结操作方法(forEach / count)、收集操作方法(collect + Collectors.toList / toSet / toMap)、Stream 链式调用与分页思想,以及 Stream 中 Lambda 表达式(Predicate / Consumer / Function 函数式接口)与方法引用的应用等知识点。 > > 题目分为 **基础练习**(生成流 / 中间操作 / 终结操作 / 收集操作,6 题)和 **进阶挑战**(Collectors.toMap 收集成 Map / 综合实战,2 题)。 --- # 第一部分:基础练习 > 以下练习围绕今日授课核心内容展开,由浅入深。 --- ## 练习 1:生成 Stream 流的四种方式 **难度**:⭐ **知识点**:`stream()` 默认方法、`Map keySet()/values()`、`Arrays.stream()`、`Stream.of(T...values)`、可变参数 **场景描述**:Stream 流操作的第一步是**获取流**(创建一条流水线,把数据放上去)。不是所有数据都能直接调 `.stream()`——Collection 体系集合可以用默认方法 `stream()`;Map 要转成 Set 间接生成;数组要用 `Arrays.stream()`;零散数据用 `Stream.of()`。 **题目要求**: ```java import java.util.*; import java.util.stream.Stream; public class StreamCreateTest { public static void main(String[] args) { // 1. Collection 体系集合:使用默认方法 stream() 生成流 List list = new ArrayList<>(List.of("张三", "李四", "王五")); Stream stream = list.stream(); // 集合.stream() // 2. Map 体系集合:把 Map 转换成 Set,间接生成流 Map map = new HashMap<>(); // TODO: 用 keySet() 取键集合生成流,赋给 keyStream // 用 values() 取值集合生成流,赋给 valueStream // 3. 数组:通过 Arrays 中的静态方法 stream 生成流 String[] array = {"郑伊健", "陈浩南", "齐夏", "谢文东"}; // TODO: Arrays.stream(array) 生成流,赋给 arrayStream // 4. 相同类型的多个数据:Stream 接口的静态方法 of(T...values) // TODO: 1) Stream stringStream = Stream.of("长贵", "赵四", "刘能", "旺财"); // 2) Stream integerStream = Stream.of(10, 15, 20, 35, 46); } } ``` **思考题**: 1. Map 为什么不能直接调用 `.stream()`?(提示:Map 不属于 Collection 体系) 2. `Stream.of(10, 15, 20, 35, 46)` 用到了之前学的什么语法?(提示:可变参数 `T...`) 3. `Arrays.stream(array)` 和 `list.stream()` 有什么区别?为什么数组不能调 `.stream()`? --- ## 练习 2:filter 过滤 + forEach 遍历(筛选集合元素) **难度**:⭐⭐ **知识点**:`filter(Predicate)`、`forEach(Consumer)`、Lambda 表达式、`startsWith()/length()` 字符串方法 **场景描述**:Stream 流中最常用的操作——**过滤**(filter)和**遍历**(forEach)。今天课上的案例:从集合中筛选出所有以"万"开头的元素,再筛选出其中长度为 3 的元素。 **题目要求**: ```java import java.util.ArrayList; import java.util.List; public class FilterTest { public static void main(String[] args) { List list = new ArrayList<>(List.of("万震山", "万梓良", "万事如意", "王老五", "万巨龙", "张三")); // 1. 用 Stream 流筛选出所有以"万"开头的元素,并遍历输出 // TODO: list.stream().filter(...).forEach(System.out::println); // 过滤条件:s -> s.startsWith("万") // 2. 再叠加一次过滤:以"万"开头 且 长度为 3 的元素,遍历输出 // TODO: 连续调用两次 filter // 3. 挑战:用 count() 统计"万"开头且长度为 3 的元素个数 // TODO: long count = list.stream()...count(); System.out.println(count); } } ``` **预期输出**: ``` ========== 以"万"开头 ========== 万震山 万梓良 万事如意 万巨龙 ========== 以"万"开头 且 长度为 3 ========== 万震山 万梓良 万巨龙 ========== 个数统计 ========== 3 ``` **思考题**: 1. `.filter()` 的参数 `s -> s.startsWith("万")` 本质是什么?(提示:`Predicate` 函数式接口 `test()` 方法的简化) 2. 为什么可以连续写两个 `.filter()`?(提示:中间方法返回新的流对象,可链式调用) 3. `.forEach(System.out::println)` 用到了什么写法?(提示:方法引用,等价于 `s -> System.out.println(s)`) --- ## 练习 3:中间方法链式调用——filter / limit / skip(分页) **难度**:⭐⭐ **知识点**:`filter`、`limit(long maxSize)`、`skip(long n)`、链式调用、分页思想 **场景描述**:中间方法执行完**返回新的流对象**,可以一直链式调用。`limit(n)` 截取前 n 个元素,`skip(m)` 跳过前 m 个元素——两者组合可以轻松实现**分页**功能(跳过 m 条、取 n 条)。 **题目要求**: ```java import java.util.stream.Stream; public class PageTest { public static void main(String[] args) { // 1. 生成长度 0~9 的整数流,截取前 5 个(limit(5)),遍历输出 → 期望 0 1 2 3 4 // TODO: Stream.of(0,1,2,3,4,5,6,7,8,9).limit(5).forEach(...) // 2. 生成长度 0~9 的整数流,跳过前 3 个(skip(3)),遍历输出 → 期望 3 4 5 6 7 8 9 // 3. 分页:集合共 10 条数据,每页 3 条 // 第 1 页:skip(0).limit(3) → 0 1 2 // 第 2 页:skip(3).limit(3) → 3 4 5 // 第 3 页:skip(6).limit(3) → 6 7 8 // 第 4 页:skip(9).limit(3) → 9 // TODO: 封装一个方法 showPage(List list, int pageNo, int pageSize) // 公式:skip((pageNo-1) * pageSize).limit(pageSize) } } ``` **预期输出**: ``` ========== limit(5) 截取前 5 个 ========== 0 1 2 3 4 ========== skip(3) 跳过前 3 个 ========== 3 4 5 6 7 8 9 ========== 分页:每页 3 条 ========== 第 1 页:0 1 2 第 2 页:3 4 5 第 3 页:6 7 8 第 4 页:9 ``` **思考题**: 1. `limit` 和 `skip` 都是中间方法吗?它们各自返回什么?(提示:返回新的流对象,可继续链式调用) 2. 分页的通用公式是什么?为什么第一页要 `skip(0)`?(提示:`skip((pageNo-1)*pageSize).limit(pageSize)`) 3. 如果数据不够一页(如第 4 页只剩 1 条),`limit(3)` 会报错吗?(提示:不会,取到末尾就结束) --- ## 练习 4:concat 合并两流 + distinct 去重 **难度**:⭐⭐ **知识点**:`Stream.concat(stream a, stream b)`、`distinct()`、`Object.equals`、不可变集合 `List.of` **场景描述**:`concat` 是 Stream 接口的**静态方法**,把两个流合并成一个流;`distinct` 是去重中间方法,依据 `Object.equals(obj)` 判断元素是否相同。 **题目要求**: ```java import java.util.List; import java.util.stream.Stream; public class ConcatDistinctTest { public static void main(String[] args) { Stream streamA = Stream.of("长贵", "赵四", "刘能"); Stream streamB = Stream.of("旺财", "长贵", "赵四"); // 1. concat:合并两个流为一个流,并遍历输出 // TODO: Stream.concat(streamA, streamB).forEach(System.out::println); // 2. distinct:去重后再遍历输出(去掉重复的"长贵""赵四") // TODO: Stream.concat(streamA, streamB).distinct().forEach(System.out::println); // 3. 给含重复元素的集合去重后遍历输出 List list = List.of("长贵", "赵四", "郑伊健", "陈浩南", "刘能", "长贵", "赵四"); // TODO: list.stream().distinct().forEach(System.out::println); } } ``` **预期输出**: ``` ========== concat 合并(有重复) ========== 长贵 赵四 刘能 旺财 长贵 赵四 ========== concat + distinct 去重 ========== 长贵 赵四 刘能 旺财 ========== List 去重 ========== 长贵 赵四 郑伊健 陈浩南 刘能 ``` **思考题**: 1. `concat` 是流对象的方法还是 Stream 接口的静态方法?怎么调用?(提示:`Stream.concat(a, b)`,不是 `a.concat(b)`) 2. `distinct` 判断重复的依据是什么?(提示:`Object.equals(obj)`——自定义对象需重写 equals/hashCode) 3. `List.of(...)` 返回的集合能 `add` 吗?(提示:不可变集合,直接修改抛 UnsupportedOperationException) --- ## 练习 5:终结操作方法——count 计数 + forEach 遍历 **难度**:⭐⭐ **知识点**:终结方法概念、`long count()`、`forEach(Consumer)`、终结方法只能有一个 **场景描述**:终结操作的意思是执行完这个方法后,**Stream 流就不能再执行其他操作**了。`forEach` 对每个元素执行动作,`count` 返回流中元素个数——它们都是流水线的**最后一步**。 **题目要求**: ```java import java.util.List; public class TerminateTest { public static void main(String[] args) { List list = List.of("万震山", "万梓良", "万事如意", "王老五", "万巨龙", "张三"); // 1. forEach 遍历:输出每个元素 // TODO: list.stream().forEach(System.out::println); // 2. count 计数:统计以"万"开头的元素个数(filter + count) // TODO: long count = list.stream().filter(s -> s.startsWith("万")).count(); // System.out.println(count); // 期望 4 // 3. 挑战:在 forEach 之后再调用 .count(),观察会发生什么? // TODO: list.stream().forEach(System.out::println).count(); // 能编译吗? } } ``` **预期输出**: ``` ========== forEach 遍历 ========== 万震山 万梓良 万事如意 王老五 万巨龙 张三 ========== count 计数 ========== 以"万"开头的有:4 个 ========== 挑战结果 ========== forEach 之后再调 count() 会编译报错 —— 终结方法只能有一个,且调用后流已结束 ``` **思考题**: 1. `forEach` 和 `count` 属于三类方法中的哪一类?(提示:终结方法) 2. `count()` 的返回值是什么类型?(提示:`long`) 3. 为什么一条流水线只能有一个终结方法?(提示:终结方法执行后流就结束了) --- ## 练习 6:collect 收集操作——Collectors.toList() / toSet() **难度**:⭐⭐⭐ **知识点**:`collect(Collector)`、`Collectors.toList()`、`Collectors.toSet()`、收集结果不可变、Set 天然去重 **场景描述**:使用流对数据操作完毕后,可以**把流中的数据收集到集合**中——`collect` 是终结操作,配合工具类 `Collectors` 的 `toList()` / `toSet()` 把结果存起来,方便后续继续使用。 **题目要求**: ```java import java.util.List; import java.util.Set; import java.util.stream.Collectors; public class CollectTest { public static void main(String[] args) { List list = List.of("长贵", "赵四", "郑伊健", "陈浩南", "刘能", "郑伊健", "陈浩南", "郑伊健", "陈浩南"); // 1. 去重后收集成 List 集合 // TODO: List list1 = list.stream().distinct().collect(Collectors.toList()); // System.out.println(list1); // 2. 过滤"伊"后收集成 Set 集合(Set 天然去重) // TODO: Set set = list.stream().filter(s -> s.contains("伊")) // .collect(Collectors.toSet()); // System.out.println(set); // 3. 挑战:验证 toList() 返回的集合不可变——尝试 list1.add("旺财") // TODO: 观察抛出的异常类型(UnsupportedOperationException) } } ``` **预期输出**: ``` ========== distinct + toList ========== [长贵, 赵四, 郑伊健, 陈浩南, 刘能] ========== filter + toSet ========== [郑伊健, 陈浩南] ========== 挑战 ========== Exception in thread "main" java.lang.UnsupportedOperationException ``` **思考题**: 1. `collect` 是中间操作还是终结操作?(提示:终结操作,执行完流结束,把结果收集到集合) 2. `Collectors` 是什么?(提示:收集工具类,类比 Arrays / Collections) 3. `toList()` 和 `toSet()` 返回的集合有什么特点?(提示:toList 不可变;toSet 天然去重且无序) --- # 第二部分:进阶挑战 > 以下练习综合运用 Stream 流 + 实体类 + Collectors 收集器。 --- ## 进阶 1:Collectors.toMap 收集成 Map(学生信息映射) **难度**:⭐⭐⭐⭐ **知识点**:`Collectors.toMap(keyMapper, valueMapper)`、`Function` 函数式接口、Lambda vs 匿名内部类、`Map` **场景描述**:把流中的学生对象收集成 `Map`——键是**学号**(id),值是**学生对象**本身。`toMap` 的两个参数都是 `Function` 函数式接口:第一个从元素中取"键",第二个从元素中取"值"。 **题目要求**: 1. 定义 `Student` 实体类: ```java import java.util.Objects; public class Student { private Integer id; // 学号 private String name; // 姓名 // TODO: 无参构造 + 有参构造(id, name) // TODO: getter / setter // TODO: equals / hashCode —— 基于 id 和 name(Objects.equals / Objects.hash) // TODO: toString } ``` 2. 编写测试类 `StudentMapTest`: ```java import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; public class StudentMapTest { public static void main(String[] args) { Student s1 = new Student(1001, "项羽"); Student s2 = new Student(1002, "张良"); Student s3 = new Student(1003, "项庄"); Student s4 = new Student(1004, "刘邦"); Student s5 = new Student(1005, "萧何"); Student s6 = new Student(1006, "韩信"); Student s7 = new Student(1007, "樊哙"); ArrayList list = new ArrayList<>(List.of(s1, s2, s3, s4, s5, s6, s7)); // 1. Lambda 写法:toMap(键, 值) // TODO: Map map = list.stream() // .collect(Collectors.toMap( // student -> student.getId(), // 键:学号 // student -> student // 值:学生对象 // )); // System.out.println(map); // 2. 匿名内部类写法:Function 函数式接口(与 Lambda 等价) // TODO: Map map2 = list.stream() // .collect(Collectors.toMap( // new Function() { // public Integer apply(Student student) { return student.getId(); } // }, // new Function() { // public Student apply(Student student) { return student; } // } // )); // 3. 挑战:用 get(1003) 取出"项庄",输出学生姓名 // TODO: System.out.println(map.get(1003).getName()); // 期望 项庄 } } ``` **预期输出**: ``` {1001=Student{id=1001, name='项羽'}, 1002=Student{id=1002, name='张良'}, ...} ========== 按学号取值 ========== 项庄 ``` **原理分析(必答)**: 1. `Collectors.toMap` 的两个参数分别是什么?(提示:keyMapper 取键、valueMapper 取值,都是 `Function` 函数式接口) 2. `student -> student.getId()` 与匿名内部类 `new Function(){...}` 是什么关系?(提示:等价,Lambda 是后者的简化) 3. 如果两个学生学号相同(id 重复),`toMap` 会发生什么?(提示:默认抛 IllegalStateException——键必须唯一) --- ## 进阶 2:综合实战——员工信息流式处理 **难度**:⭐⭐⭐⭐ **知识点**:filter / distinct / count / collect / toMap 综合运用、`contains()` 判断、Stream 全流程 **场景描述**:某公司有一批员工数据(工号、姓名、部门),用 Stream 流完成以下任务:过滤出技术部员工、按姓名去重、统计人数、收集成 Map(键=工号,值=姓名)。 **题目要求**: 1. 定义 `Employee` 类: ```java public class Employee { private String id; // 工号 private String name; // 姓名 private String dept; // 部门 public Employee(String id, String name, String dept) { this.id = id; this.name = name; this.dept = dept; } // TODO: getter / setter / toString } ``` 2. 编写测试类 `EmployeeStreamTest`: ```java import java.util.*; import java.util.stream.Collectors; public class EmployeeStreamTest { public static void main(String[] args) { List list = new ArrayList<>(List.of( new Employee("E001", "张三", "技术部"), new Employee("E002", "李四", "市场部"), new Employee("E003", "王五", "技术部"), new Employee("E004", "张三", "技术部"), // 姓名重复,测试去重 new Employee("E005", "赵六", "财务部") )); // 1. 过滤:只保留"技术部"的员工,遍历输出 // TODO: list.stream().filter(e -> "技术部".equals(e.getDept())).forEach(...) // 2. 去重:按姓名去重(先 map 取出姓名,再 distinct) // TODO: list.stream().map(Employee::getName).distinct().forEach(System.out::println); // 期望:张三 王五 李四 赵六(注意:张三只出现一次) // 3. 统计:技术部员工个数(filter + count) // TODO: long cnt = list.stream().filter(e -> "技术部".equals(e.getDept())).count(); // 4. 收集:收集成 Map,键=工号,值=姓名 // TODO: Map map = list.stream() // .collect(Collectors.toMap(Employee::getId, Employee::getName)); // System.out.println(map); } } ``` **预期输出**: ``` ========== 技术部员工 ========== E001-张三-技术部 E003-王五-技术部 E004-张三-技术部 ========== 按姓名去重 ========== 张三 王五 李四 赵六 ========== 技术部人数统计 ========== 3 ========== 收集成 Map(工号→姓名) ========== {E001=张三, E002=李四, E003=王五, E004=张三, E005=赵六} ``` **原理分析(必答)**: 1. `Employee::getName` 是什么写法?等价于什么 Lambda?(提示:方法引用,等价于 `e -> e.getName()`) 2. 任务 4 中 `toMap(Employee::getId, Employee::getName)` 的两个参数分别是什么?(提示:键=工号、值=姓名,都是方法引用) 3. 任务 3 为什么能"先 filter 再 count"统计人数?(提示:filter 是中间方法返回新流,count 是终结方法统计个数) --- # 参考答案要点 ## 练习 1 参考 ```java // 2. Map 体系集合 Stream keyStream = map.keySet().stream(); // key 就是 Set 集合 Stream valueStream = map.values().stream(); // value 就是 List 集合 // 3. 数组 Stream arrayStream = Arrays.stream(array); // 4. 相同类型的多个数据(Stream.of 本质是可变参数 T...) Stream stringStream = Stream.of("长贵", "赵四", "刘能", "旺财"); Stream integerStream = Stream.of(10, 15, 20, 35, 46); ``` ## 练习 2 参考 ```java // 1. 以"万"开头 list.stream().filter(s -> s.startsWith("万")).forEach(System.out::println); // 2. 以"万"开头 且 长度为 3 list.stream() .filter(s -> s.startsWith("万")) .filter(s -> s.length() == 3) .forEach(System.out::println); // 3. 统计个数 long count = list.stream() .filter(s -> s.startsWith("万")) .filter(s -> s.length() == 3) .count(); System.out.println("个数:" + count); // 3 ``` ## 练习 3 参考 ```java // 1. limit(5):截取前 5 个 Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) .limit(5) .forEach(n -> System.out.print(n + " ")); // 2. skip(3):跳过前 3 个 Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) .skip(3) .forEach(n -> System.out.print(n + " ")); // 3. 分页方法:skip((pageNo-1)*pageSize).limit(pageSize) public void showPage(List list, int pageNo, int pageSize) { list.stream() .skip((pageNo - 1) * pageSize) // 跳过前面的页 .limit(pageSize) // 截取本页数量 .forEach(n -> System.out.print(n + " ")); System.out.println(); } ``` ## 练习 4 参考 ```java // 1. concat 合并(含重复) Stream.concat(streamA, streamB).forEach(System.out::println); // 长贵 赵四 刘能 旺财 长贵 赵四 // 2. concat + distinct 去重 Stream.concat(streamA, streamB).distinct().forEach(System.out::println); // 长贵 赵四 刘能 旺财 // 3. List 去重 list.stream().distinct().forEach(System.out::println); // 长贵 赵四 郑伊健 陈浩南 刘能 ``` ## 练习 5 参考 ```java // 1. forEach 遍历 list.stream().forEach(System.out::println); // 2. count 计数 long count = list.stream().filter(s -> s.startsWith("万")).count(); System.out.println("以\"万\"开头的有:" + count + " 个"); // 3. 挑战:forEach 后再调 count() → 编译报错 // 原因:forEach 是终结方法,调用后流已结束,不能再继续链式操作 // list.stream().forEach(System.out::println).count(); // ✗ 编译错误 ``` ## 练习 6 参考 ```java // 1. 去重后收集成 List List list1 = list.stream().distinct().collect(Collectors.toList()); System.out.println(list1); // 2. 过滤"伊"后收集成 Set Set set = list.stream() .filter(s -> s.contains("伊")) .collect(Collectors.toSet()); System.out.println(set); // 3. 挑战:toList() 返回不可变集合,add 抛 UnsupportedOperationException // list1.add("旺财"); // ✗ 运行时抛 UnsupportedOperationException ``` ## 进阶 1 参考(核心逻辑) ```java // Student:equals/hashCode 基于 id + name @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return Objects.equals(id, student.id) && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(id, name); } @Override public String toString() { return "Student{id=" + id + ", name='" + name + "'}"; } // StudentMapTest:Lambda 方式收集成 Map Map map = list.stream() .collect(Collectors.toMap( student -> student.getId(), // 键:学号 student -> student // 值:学生对象 )); System.out.println(map); System.out.println(map.get(1003).getName()); // 项庄 ``` ## 进阶 2 参考(核心逻辑) ```java // 1. 过滤技术部 list.stream() .filter(e -> "技术部".equals(e.getDept())) .forEach(System.out::println); // 2. 按姓名去重(map 取出姓名 → distinct 去重) list.stream() .map(Employee::getName) .distinct() .forEach(System.out::println); // 3. 统计技术部人数 long cnt = list.stream() .filter(e -> "技术部".equals(e.getDept())) .count(); // 4. 收集成 Map(键=工号,值=姓名) Map map = list.stream() .collect(Collectors.toMap(Employee::getId, Employee::getName)); System.out.println(map); ``` --- # 涵盖知识点总览 | 知识点 | 对应练习 | 说明 | |--------|---------|------| | 生成流的四种方式 | 练习 1 | Collection.stream() / Map keySet/values / Arrays.stream / Stream.of | | Stream.of 与可变参数 | 练习 1 | `of(T...values)` 本质是可变参数,可传任意个数数据 | | filter 过滤(Predicate) | 练习 2、3、5、6、进阶 2 | 传入断言条件,返回符合条件的新流 | | forEach 遍历(Consumer) | 练习 2、5 | 对每个元素执行动作,方法引用 System.out::println | | 中间方法链式调用 | 练习 3 | 中间方法返回新流对象,可一直链式 | | limit 截取 / skip 跳过 | 练习 3 | 分页公式 skip((n-1)*size).limit(size) | | concat 合并两流 | 练习 4 | Stream 接口静态方法 Stream.concat(a, b) | | distinct 去重 | 练习 4、6 | 依据 Object.equals(obj) | | 终结方法只能有一个 | 练习 5 | forEach/count 调用后流结束 | | count 计数(long) | 练习 5、进阶 2 | 返回元素个数,类型 long | | collect 收集操作 | 练习 6、进阶 1、进阶 2 | 把流结果收集到集合,配合 Collectors | | Collectors.toList() / toSet() | 练习 6 | toList 不可变;toSet 天然去重 | | Collectors.toMap(keyMapper, valueMapper) | 进阶 1、进阶 2 | 两个 Function 参数:取键 / 取值 | | Function 函数式接口 | 进阶 1 | 有参有返回值;Lambda vs 匿名内部类 | | 方法引用 类::方法 | 进阶 2 | Employee::getName ≈ e -> e.getName() | | Stream + 实体类综合 | 进阶 1、进阶 2 | 实体类 + equals/hashCode + 收集成 Map |