说明:以下习题围绕今日授课核心内容——函数式编程与 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 题)。
以下练习围绕今日授课核心内容展开,由浅入深。
难度:⭐
知识点:stream() 默认方法、Map keySet()/values()、Arrays.stream()、Stream.of(T...values)、可变参数
场景描述:Stream 流操作的第一步是获取流(创建一条流水线,把数据放上去)。不是所有数据都能直接调 .stream()——Collection 体系集合可以用默认方法 stream();Map 要转成 Set 间接生成;数组要用 Arrays.stream();零散数据用 Stream.of()。
题目要求:
import java.util.*;
import java.util.stream.Stream;
public class StreamCreateTest {
public static void main(String[] args) {
// 1. Collection 体系集合:使用默认方法 stream() 生成流
List<String> list = new ArrayList<>(List.of("张三", "李四", "王五"));
Stream<String> stream = list.stream(); // 集合.stream()
// 2. Map 体系集合:把 Map 转换成 Set,间接生成流
Map<String, Object> 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<String> stringStream = Stream.of("长贵", "赵四", "刘能", "旺财");
// 2) Stream<Integer> integerStream = Stream.of(10, 15, 20, 35, 46);
}
}
思考题:
.stream()?(提示:Map 不属于 Collection 体系)Stream.of(10, 15, 20, 35, 46) 用到了之前学的什么语法?(提示:可变参数 T...)Arrays.stream(array) 和 list.stream() 有什么区别?为什么数组不能调 .stream()?难度:⭐⭐
知识点:filter(Predicate)、forEach(Consumer)、Lambda 表达式、startsWith()/length() 字符串方法
场景描述:Stream 流中最常用的操作——过滤(filter)和遍历(forEach)。今天课上的案例:从集合中筛选出所有以"万"开头的元素,再筛选出其中长度为 3 的元素。
题目要求:
import java.util.ArrayList;
import java.util.List;
public class FilterTest {
public static void main(String[] args) {
List<String> 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
思考题:
.filter() 的参数 s -> s.startsWith("万") 本质是什么?(提示:Predicate<String> 函数式接口 test() 方法的简化).filter()?(提示:中间方法返回新的流对象,可链式调用).forEach(System.out::println) 用到了什么写法?(提示:方法引用,等价于 s -> System.out.println(s))难度:⭐⭐
知识点:filter、limit(long maxSize)、skip(long n)、链式调用、分页思想
场景描述:中间方法执行完返回新的流对象,可以一直链式调用。limit(n) 截取前 n 个元素,skip(m) 跳过前 m 个元素——两者组合可以轻松实现分页功能(跳过 m 条、取 n 条)。
题目要求:
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<Integer> 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
思考题:
limit 和 skip 都是中间方法吗?它们各自返回什么?(提示:返回新的流对象,可继续链式调用)skip(0)?(提示:skip((pageNo-1)*pageSize).limit(pageSize))limit(3) 会报错吗?(提示:不会,取到末尾就结束)难度:⭐⭐
知识点:Stream.concat(stream a, stream b)、distinct()、Object.equals、不可变集合 List.of
场景描述:concat 是 Stream 接口的静态方法,把两个流合并成一个流;distinct 是去重中间方法,依据 Object.equals(obj) 判断元素是否相同。
题目要求:
import java.util.List;
import java.util.stream.Stream;
public class ConcatDistinctTest {
public static void main(String[] args) {
Stream<String> streamA = Stream.of("长贵", "赵四", "刘能");
Stream<String> 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<String> list = List.of("长贵", "赵四", "郑伊健", "陈浩南", "刘能", "长贵", "赵四");
// TODO: list.stream().distinct().forEach(System.out::println);
}
}
预期输出:
========== concat 合并(有重复) ==========
长贵 赵四 刘能 旺财 长贵 赵四
========== concat + distinct 去重 ==========
长贵 赵四 刘能 旺财
========== List 去重 ==========
长贵 赵四 郑伊健 陈浩南 刘能
思考题:
concat 是流对象的方法还是 Stream 接口的静态方法?怎么调用?(提示:Stream.concat(a, b),不是 a.concat(b))distinct 判断重复的依据是什么?(提示:Object.equals(obj)——自定义对象需重写 equals/hashCode)List.of(...) 返回的集合能 add 吗?(提示:不可变集合,直接修改抛 UnsupportedOperationException)难度:⭐⭐
知识点:终结方法概念、long count()、forEach(Consumer)、终结方法只能有一个
场景描述:终结操作的意思是执行完这个方法后,Stream 流就不能再执行其他操作了。forEach 对每个元素执行动作,count 返回流中元素个数——它们都是流水线的最后一步。
题目要求:
import java.util.List;
public class TerminateTest {
public static void main(String[] args) {
List<String> 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() 会编译报错 —— 终结方法只能有一个,且调用后流已结束
思考题:
forEach 和 count 属于三类方法中的哪一类?(提示:终结方法)count() 的返回值是什么类型?(提示:long)难度:⭐⭐⭐
知识点:collect(Collector)、Collectors.toList()、Collectors.toSet()、收集结果不可变、Set 天然去重
场景描述:使用流对数据操作完毕后,可以把流中的数据收集到集合中——collect 是终结操作,配合工具类 Collectors 的 toList() / toSet() 把结果存起来,方便后续继续使用。
题目要求:
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class CollectTest {
public static void main(String[] args) {
List<String> list = List.of("长贵", "赵四", "郑伊健", "陈浩南", "刘能",
"郑伊健", "陈浩南", "郑伊健", "陈浩南");
// 1. 去重后收集成 List 集合
// TODO: List<String> list1 = list.stream().distinct().collect(Collectors.toList());
// System.out.println(list1);
// 2. 过滤"伊"后收集成 Set 集合(Set 天然去重)
// TODO: Set<String> 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
思考题:
collect 是中间操作还是终结操作?(提示:终结操作,执行完流结束,把结果收集到集合)Collectors 是什么?(提示:收集工具类,类比 Arrays / Collections)toList() 和 toSet() 返回的集合有什么特点?(提示:toList 不可变;toSet 天然去重且无序)以下练习综合运用 Stream 流 + 实体类 + Collectors 收集器。
难度:⭐⭐⭐⭐
知识点:Collectors.toMap(keyMapper, valueMapper)、Function<T, R> 函数式接口、Lambda vs 匿名内部类、Map<Integer, Student>
场景描述:把流中的学生对象收集成 Map<Integer, Student>——键是学号(id),值是学生对象本身。toMap 的两个参数都是 Function 函数式接口:第一个从元素中取"键",第二个从元素中取"值"。
题目要求:
定义 Student 实体类:
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
}
编写测试类 StudentMapTest:
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<Student> list = new ArrayList<>(List.of(s1, s2, s3, s4, s5, s6, s7));
// 1. Lambda 写法:toMap(键, 值)
// TODO: Map<Integer, Student> map = list.stream()
// .collect(Collectors.toMap(
// student -> student.getId(), // 键:学号
// student -> student // 值:学生对象
// ));
// System.out.println(map);
// 2. 匿名内部类写法:Function 函数式接口(与 Lambda 等价)
// TODO: Map<Integer, Student> map2 = list.stream()
// .collect(Collectors.toMap(
// new Function<Student, Integer>() {
// public Integer apply(Student student) { return student.getId(); }
// },
// new Function<Student, Student>() {
// 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='张良'}, ...}
========== 按学号取值 ==========
项庄
原理分析(必答):
Collectors.toMap 的两个参数分别是什么?(提示:keyMapper 取键、valueMapper 取值,都是 Function 函数式接口)student -> student.getId() 与匿名内部类 new Function<Student, Integer>(){...} 是什么关系?(提示:等价,Lambda 是后者的简化)toMap 会发生什么?(提示:默认抛 IllegalStateException——键必须唯一)难度:⭐⭐⭐⭐
知识点:filter / distinct / count / collect / toMap 综合运用、contains() 判断、Stream 全流程
场景描述:某公司有一批员工数据(工号、姓名、部门),用 Stream 流完成以下任务:过滤出技术部员工、按姓名去重、统计人数、收集成 Map(键=工号,值=姓名)。
题目要求:
定义 Employee 类:
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
}
编写测试类 EmployeeStreamTest:
import java.util.*;
import java.util.stream.Collectors;
public class EmployeeStreamTest {
public static void main(String[] args) {
List<Employee> 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<String, String>,键=工号,值=姓名
// TODO: Map<String, String> map = list.stream()
// .collect(Collectors.toMap(Employee::getId, Employee::getName));
// System.out.println(map);
}
}
预期输出:
========== 技术部员工 ==========
E001-张三-技术部
E003-王五-技术部
E004-张三-技术部
========== 按姓名去重 ==========
张三 王五 李四 赵六
========== 技术部人数统计 ==========
3
========== 收集成 Map(工号→姓名) ==========
{E001=张三, E002=李四, E003=王五, E004=张三, E005=赵六}
原理分析(必答):
Employee::getName 是什么写法?等价于什么 Lambda?(提示:方法引用,等价于 e -> e.getName())toMap(Employee::getId, Employee::getName) 的两个参数分别是什么?(提示:键=工号、值=姓名,都是方法引用)// 2. Map 体系集合
Stream<String> keyStream = map.keySet().stream(); // key 就是 Set 集合
Stream<Object> valueStream = map.values().stream(); // value 就是 List 集合
// 3. 数组
Stream<String> arrayStream = Arrays.stream(array);
// 4. 相同类型的多个数据(Stream.of 本质是可变参数 T...)
Stream<String> stringStream = Stream.of("长贵", "赵四", "刘能", "旺财");
Stream<Integer> integerStream = Stream.of(10, 15, 20, 35, 46);
// 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
// 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<Integer> list, int pageNo, int pageSize) {
list.stream()
.skip((pageNo - 1) * pageSize) // 跳过前面的页
.limit(pageSize) // 截取本页数量
.forEach(n -> System.out.print(n + " "));
System.out.println();
}
// 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);
// 长贵 赵四 郑伊健 陈浩南 刘能
// 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(); // ✗ 编译错误
// 1. 去重后收集成 List
List<String> list1 = list.stream().distinct().collect(Collectors.toList());
System.out.println(list1);
// 2. 过滤"伊"后收集成 Set
Set<String> set = list.stream()
.filter(s -> s.contains("伊"))
.collect(Collectors.toSet());
System.out.println(set);
// 3. 挑战:toList() 返回不可变集合,add 抛 UnsupportedOperationException
// list1.add("旺财"); // ✗ 运行时抛 UnsupportedOperationException
// 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<Integer, Student> map = list.stream()
.collect(Collectors.toMap(
student -> student.getId(), // 键:学号
student -> student // 值:学生对象
));
System.out.println(map);
System.out.println(map.get(1003).getName()); // 项庄
// 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<String, String> 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 |