第 13 课:Lambda、函数式接口、方法引用与 Stream
适用环境:Java 17。本文按零基础编写,所有例子都说明“为什么写、每行做什么、结果是什么”。
本课目标
学完后,你应该能够:
- 解释 Lambda 为什么出现,而不只是背
->。 - 判断一个接口能不能接收 Lambda。
- 正确使用
Predicate、Function、Consumer、Supplier。 - 看懂四种方法引用,并能还原成 Lambda。
- 区分集合和 Stream,理解惰性执行与一次消费。
- 使用
filter、map、sorted、reduce、collect处理数据。 - 看懂
Optional,避免随意调用get()。 - 完成一个员工数据统计综合案例,并能解释样例输出。
1. Lambda 到底解决了什么问题
假设我们需要把名字列表按照长度排序。sort 方法需要的不只是数据,还需要一段“比较规则”。在 Lambda 出现之前,经常要写匿名内部类:
List<String> names = new ArrayList<>(List.of("Tom", "Alexander", "Li"));
// Comparator<String> 是比较两个字符串的规则对象。
names.sort(new Comparator<String>() {
@Override
public int compare(String left, String right) {
// 返回负数:left 排在前面;返回正数:right 排在前面;0:两者相等。
return Integer.compare(left.length(), right.length());
}
});
真正有用的只有这一行:
return Integer.compare(left.length(), right.length());
其他代码只是为了创建一个实现 Comparator 的对象。Lambda 允许我们直接描述这段行为:
names.sort((left, right) -> Integer.compare(left.length(), right.length()));
可以把 Lambda 暂时理解成:
参数 -> 使用这些参数完成的行为
Lambda 不是独立存在的“无类型函数”。它最终必须对应某个函数式接口,由编译器根据上下文确定类型。
2. 函数式接口:Lambda 能够存在的前提
函数式接口是“只有一个抽象方法”的接口。它仍然可以有任意数量的 default 方法和 static 方法。
@FunctionalInterface
interface Calculator {
// 唯一的抽象方法:接收两个 int,返回一个 int。
int calculate(int left, int right);
// default 方法不影响函数式接口资格。
default String description() {
return "整数计算规则";
}
// static 方法也不影响函数式接口资格。
static void printTitle() {
System.out.println("开始计算");
}
}
使用 Lambda 提供不同计算行为:
Calculator add = (left, right) -> left + right; // 加法规则
Calculator multiply = (left, right) -> left * right; // 乘法规则
System.out.println(add.calculate(3, 5)); // 输出 8
System.out.println(multiply.calculate(3, 5)); // 输出 15
执行过程:
add的类型是Calculator。- 编译器查看
Calculator.calculate(int, int),知道 Lambda 必须接收两个整数并返回整数。 - 调用
add.calculate(3, 5)时,执行left + right。
@FunctionalInterface 不是必需语法,但强烈建议加上。以后如果误加第二个抽象方法,编译器会马上报错。
3. Lambda 语法从完整到简写
3.1 无参数
Runnable task = () -> System.out.println("任务执行了");
// ^^ 无参数也必须写一对小括号
task.run(); // 输出:任务执行了
3.2 一个参数
Consumer<String> printer1 = (text) -> System.out.println(text);
Consumer<String> printer2 = text -> System.out.println(text);
// 一个参数且不显式写类型时,可以省略小括号。
3.3 多个参数
BinaryOperator<Integer> add = (left, right) -> left + right;
// 多个参数必须使用小括号包围。
3.4 单表达式和代码块
// 单表达式:自动把表达式结果作为返回值,不写 return 和大括号。
Function<Integer, Integer> square1 = number -> number * number;
// 代码块:有多条语句,必须写大括号;需要返回值时必须写 return。
Function<Integer, Integer> square2 = number -> {
int result = number * number; // 先计算
return result; // 再返回
};
常见错误:
// 错误:使用大括号后,需要返回值却没有写 return。
// Function<Integer, Integer> wrong = n -> { n * n; };
4. 目标类型:同一个 Lambda 为什么能代表不同接口
Lambda 的类型由它所在的位置决定,这叫目标类型。
Predicate<String> isEmpty = text -> text.isEmpty();
Function<String, Boolean> checkEmpty = text -> text.isEmpty();
两段 Lambda 的身体相同,但类型不同:
Predicate<String>的抽象方法是boolean test(String value)。Function<String, Boolean>的抽象方法是Boolean apply(String value)。
因此 Lambda 通常不能脱离目标类型单独赋值:
// var rule = text -> text.isEmpty(); // 编译器不知道它是哪一种函数式接口
看 Lambda 时不要只看右边,要先看左边变量类型或方法参数类型。
5. Lambda 捕获局部变量:effectively final
Lambda 可以读取外部局部变量,但这个变量必须是 final 或“事实上没有再修改”。
int passingScore = 60; // 后面没有重新赋值,所以是 effectively final
Predicate<Integer> passed = score -> score >= passingScore;
System.out.println(passed.test(75)); // true
// passingScore = 70; // 如果取消注释,前面的 Lambda 会编译失败
为什么限制修改?局部变量原本存在方法栈中,方法结束后就会消失;Lambda 可能活得更久。Java 通过捕获一个稳定的值来避免“外部变量到底是哪一刻的值”这种混乱。
注意:对象引用没有重新赋值,不代表对象内容不可变:
List<String> messages = new ArrayList<>(); // 引用本身没有重新指向别处
Runnable addMessage = () -> messages.add("hello"); // 可以修改对象内部内容
虽然语法允许,但 Stream 中依赖或修改外部可变状态通常会让代码难懂,在并行环境还可能出错。
6. 四个最重要的标准函数式接口
JDK 把常见行为放在 java.util.function 包中。先记住四个核心接口:
| 接口 | 抽象方法 | 输入 | 输出 | 典型用途 |
|---|---|---|---|---|
Predicate<T> | boolean test(T t) | 1 个 | boolean | 判断、筛选 |
Function<T,R> | R apply(T t) | 1 个 | 1 个结果 | 转换 |
Consumer<T> | void accept(T t) | 1 个 | 无 | 打印、保存、发送 |
Supplier<T> | T get() | 无 | 1 个结果 | 延迟创建或提供数据 |
6.1 Predicate:问一个“是或否”的问题
Predicate<Integer> adult = age -> age >= 18;
boolean result = adult.test(20); // 把 20 交给 age -> age >= 18
System.out.println(result); // true
组合条件:
Predicate<Integer> atLeast18 = age -> age >= 18;
Predicate<Integer> below60 = age -> age < 60;
Predicate<Integer> workingAge = atLeast18.and(below60); // 同时满足
System.out.println(workingAge.test(30)); // true
System.out.println(workingAge.test(70)); // false
还有 or() 表示“或者”,negate() 表示取反。
6.2 Function:把一种数据转换成另一种数据
Function<String, Integer> length = text -> text.length();
// 输入类型 输出类型
int result = length.apply("Java");
System.out.println(result); // 4
andThen 表示先做当前转换,再做下一次转换:
Function<String, String> trim = text -> text.trim();
Function<String, Integer> length = text -> text.length();
Function<String, Integer> trimmedLength = trim.andThen(length);
System.out.println(trimmedLength.apply(" Java ")); // 4
6.3 Consumer:接收数据并执行动作,不返回结果
Consumer<String> printer = text -> System.out.println("收到:" + text);
printer.accept("Java"); // 输出:收到:Java
6.4 Supplier:没有输入,产生一个结果
Supplier<List<String>> listFactory = () -> new ArrayList<>();
List<String> list = listFactory.get(); // 需要时才真正创建 ArrayList
6.5 其他常用接口
UnaryOperator<T>:输入和输出是同一种类型,如n -> n * 2。BinaryOperator<T>:两个同类型输入,返回同类型结果,如(a, b) -> a + b。BiPredicate<T,U>、BiFunction<T,U,R>、BiConsumer<T,U>:接收两个参数。IntPredicate、IntFunction<R>、IntConsumer:基本类型专用版本,减少装箱。
7. 方法引用:给已有方法起“快捷入口”
当 Lambda 只是把参数原样交给一个现有方法时,可以使用 ::。
Consumer<String> lambda = text -> System.out.println(text);
Consumer<String> reference = System.out::println;
两者行为相同。方法引用不是“立即调用方法”,而是把调用方式保存起来,等 accept() 执行时才调用。
四种方法引用
| 类型 | 方法引用 | 对应 Lambda |
|---|---|---|
| 静态方法 | Integer::parseInt | text -> Integer.parseInt(text) |
| 特定对象的实例方法 | System.out::println | text -> System.out.println(text) |
| 任意对象的实例方法 | String::length | text -> text.length() |
| 构造方法 | ArrayList::new | () -> new ArrayList<>() |
完整例子:
Function<String, Integer> parser = Integer::parseInt;
Function<String, Integer> length = String::length;
Consumer<String> printer = System.out::println;
Supplier<List<String>> factory = ArrayList::new;
System.out.println(parser.apply("123")); // 123
System.out.println(length.apply("Java")); // 4
printer.accept("你好"); // 你好
System.out.println(factory.get()); // []
方法引用不是越短越好。如果看不出参数如何传递,保留 Lambda 反而更清楚。
8. Comparator 与 Lambda
排序是 Lambda 最常见的入口之一。
record Student(String name, int score) {}
List<Student> students = new ArrayList<>(List.of(
new Student("小明", 88),
new Student("小红", 95),
new Student("小李", 88)
));
// 先按分数降序;分数相同时,再按姓名升序。
students.sort(
Comparator.comparingInt(Student::score) // 先提取 int 分数
.reversed() // 把默认升序反转为降序
.thenComparing(Student::name) // 分数相同时比较姓名
);
不要写 (a, b) -> b.score() - a.score(),因为极端整数可能发生溢出。使用 Comparator.comparingInt 或 Integer.compare 更安全、更清楚。
9. Stream 是什么,不是什么
集合负责存储数据;Stream 负责描述怎样处理数据。Stream 自己不存储元素。
集合/数组等数据源
↓
创建 Stream
↓
中间操作:filter、map、sorted……
↓
终止操作:toList、collect、count、reduce……
↓
得到结果
最小例子:
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> result = numbers.stream() // 1. 从列表打开一条数据流
.filter(n -> n % 2 == 0) // 2. 只保留偶数:2、4
.map(n -> n * n) // 3. 每个偶数平方:4、16
.toList(); // 4. 终止操作,收集成新 List
System.out.println(result); // [4, 16]
System.out.println(numbers); // [1, 2, 3, 4, 5],原列表没有被修改
Stream 的三个核心特征:
- 惰性执行:没有终止操作时,中间操作通常不会真正处理数据。
- 一次消费:终止操作后,不能再次使用同一个 Stream。
- 不修改数据源:规范用法是产生新结果,不在管道里修改原集合。
10. 惰性执行:为什么 filter 不会马上运行
List<String> names = List.of("Java", "Go", "Spring");
Stream<String> pipeline = names.stream()
.filter(name -> {
System.out.println("正在判断:" + name);
return name.length() >= 4;
});
System.out.println("管道已经创建");
long count = pipeline.count(); // 终止操作到这里才触发 filter
System.out.println("数量:" + count);
样例输出:
管道已经创建
正在判断:Java
正在判断:Go
正在判断:Spring
数量:2
这说明 filter 返回的是一条处理规则,不是已经装好结果的新集合。
11. 创建 Stream 的常用方式
List<String> list = List.of("A", "B");
Stream<String> fromList = list.stream(); // 从集合创建
Stream<String> fromValues = Stream.of("A", "B"); // 从若干值创建
IntStream fromArray = Arrays.stream(new int[]{1, 2}); // 从数组创建基本类型流
IntStream range = IntStream.rangeClosed(1, 5); // 产生 1、2、3、4、5
Map 没有 stream(),要根据需要选择视图:
Map<String, Integer> scores = Map.of("Java", 90, "SQL", 85);
scores.entrySet().stream(); // 同时处理 key 和 value
scores.keySet().stream(); // 只处理 key
scores.values().stream(); // 只处理 value
12. 常用中间操作
中间操作返回的仍然是 Stream,因此可以继续链式调用。
12.1 filter:筛选,数量可能减少,元素类型不变
List<String> result = List.of("Java", "Go", "Spring").stream()
.filter(name -> name.length() >= 4) // 条件为 true 的元素留下
.toList();
System.out.println(result); // [Java, Spring]
12.2 map:转换,数量通常不变,类型可以改变
List<Integer> lengths = List.of("Java", "Go", "Spring").stream()
.map(String::length) // String 转换为 Integer
.toList();
System.out.println(lengths); // [4, 2, 6]
记忆:filter 回答“要不要”,map 回答“变成什么”。
12.3 flatMap:把多层数据摊平成一层
List<List<String>> classes = List.of(
List.of("小明", "小红"),
List.of("小李", "小王")
);
List<String> allStudents = classes.stream()
.flatMap(List::stream) // 每个 List<String> 打开成 Stream<String>,再合并
.toList();
System.out.println(allStudents); // [小明, 小红, 小李, 小王]
map(List::stream) 会得到 Stream<Stream<String>>;flatMap 才会把它压平为 Stream<String>。
12.4 distinct、sorted、limit、skip
List<Integer> result = Stream.of(5, 2, 5, 1, 3, 2)
.distinct() // 去重:5、2、1、3
.sorted() // 升序:1、2、3、5
.skip(1) // 跳过第一个:2、3、5
.limit(2) // 最多取两个:2、3
.toList();
System.out.println(result); // [2, 3]
12.5 peek:主要用于观察,不要承担业务修改
List<Integer> result = Stream.of(1, 2, 3)
.peek(n -> System.out.println("map 前:" + n))
.map(n -> n * 10)
.peek(n -> System.out.println("map 后:" + n))
.toList();
peek 常用于学习或调试管道。不要依赖它修改数据库、发送消息或改变外部集合,因为 Stream 可能短路或优化某些步骤。
13. 常用终止操作
终止操作会触发整个管道,并返回非 Stream 结果或产生最终动作。
13.1 forEach、count
long count = Stream.of("Java", "Go", "Spring")
.filter(name -> name.length() >= 4)
.count();
System.out.println(count); // 2
Stream.of("A", "B").forEach(System.out::println); // 分别打印 A、B
13.2 anyMatch、allMatch、noneMatch
List<Integer> scores = List.of(90, 75, 58);
boolean hasFailed = scores.stream().anyMatch(score -> score < 60); // 至少一个不及格
boolean allPassed = scores.stream().allMatch(score -> score >= 60); // 全部及格
boolean noneInvalid = scores.stream().noneMatch(score -> score < 0 || score > 100);
这些操作可以短路:anyMatch 找到第一个满足条件的元素后就能停止。
13.3 reduce:把多个元素合并为一个结果
int total = Stream.of(10, 20, 30)
.reduce(0, (sum, number) -> sum + number);
System.out.println(total); // 60
执行过程:
初始 sum = 0
0 + 10 = 10
10 + 20 = 30
30 + 30 = 60
这里的 0 是恒等值:0 + x 仍然是 x。乘法的恒等值应该是 1,不是 0。
14. Optional:结果可能不存在
空 Stream 没有最大值、最小值或第一个元素,因此这些方法不能总返回普通对象,而是返回 Optional<T>。
Optional<Integer> maximum = Stream.of(7, 2, 9)
.max(Integer::compareTo);
maximum.ifPresent(value -> System.out.println("最大值:" + value));
int safeValue = maximum.orElse(0); // 不存在时使用 0
推荐的初学者用法:
orElse(defaultValue):没有值时用默认值。orElseGet(supplier):没有值时才计算默认值。ifPresent(consumer):有值才执行动作。orElseThrow():业务要求必须存在,不存在就抛异常。
不要养成直接 optional.get() 的习惯;空 Optional 会抛 NoSuchElementException。
15. collect 与 Collectors
reduce 更适合得到一个值;collect 更适合收集成 List、Set、Map 或分组结果。
15.1 收集成 List、Set 和字符串
List<String> list = Stream.of("Java", "Go", "Java").toList();
Set<String> set = Stream.of("Java", "Go", "Java")
.collect(Collectors.toSet()); // 自动去重
String text = Stream.of("Java", "SQL", "Git")
.collect(Collectors.joining(", ", "技能:[", "]"));
System.out.println(text); // 技能:[Java, SQL, Git]
Java 17 的 Stream.toList() 返回不可修改 List。需要可变列表时:
List<String> mutable = stream.collect(Collectors.toCollection(ArrayList::new));
15.2 groupingBy:分组
List<String> words = List.of("Java", "Go", "SQL", "Spring");
Map<Integer, List<String>> byLength = words.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(byLength);
// 可能显示:{2=[Go], 3=[SQL], 4=[Java], 6=[Spring]}
String::length 产生分组 key,具有相同长度的字符串进入同一个 List。
15.3 partitioningBy:按 true/false 分成两组
Map<Boolean, List<Integer>> result = Stream.of(45, 60, 88, 59)
.collect(Collectors.partitioningBy(score -> score >= 60));
System.out.println(result.get(true)); // [60, 88]
System.out.println(result.get(false)); // [45, 59]
15.4 toMap:收集成 Map 时必须考虑 key 冲突
record User(String id, String name) {}
List<User> users = List.of(
new User("U01", "小明"),
new User("U02", "小红")
);
Map<String, String> idToName = users.stream()
.collect(Collectors.toMap(
User::id, // key:用户 id
User::name // value:用户名
));
如果 key 可能重复,必须提供合并规则,否则会抛 IllegalStateException:
Map<String, Integer> highestScore = attempts.stream()
.collect(Collectors.toMap(
Attempt::studentName,
Attempt::score,
Integer::max // key 重复时保留更高分
));
16. IntStream:避免不必要的装箱
Stream<Integer> 中的整数是对象;大量数值计算可使用 IntStream。
int total = List.of(10, 20, 30).stream()
.mapToInt(Integer::intValue) // Stream<Integer> 转成 IntStream
.sum(); // IntStream 直接提供 sum()
double average = IntStream.of(80, 90, 100)
.average() // 返回 OptionalDouble,因为空流没有平均值
.orElse(0.0);
IntStream 还直接提供 sum()、average()、min()、max() 和 summaryStatistics()。
17. Stream 的高频错误
17.1 重复使用同一个 Stream
Stream<String> stream = Stream.of("A", "B");
long count = stream.count();
// stream.forEach(System.out::println); // 错:Stream 已经被消费
需要再次处理时,从原数据源重新调用 stream()。
17.2 忘记终止操作
list.stream().filter(value -> value > 0); // 只创建管道,结果没有被消费
17.3 在管道中修改数据源或外部变量
List<Integer> result = new ArrayList<>();
numbers.stream().filter(n -> n > 0).forEach(result::add); // 不推荐副作用
List<Integer> better = numbers.stream()
.filter(n -> n > 0)
.toList(); // 直接把结果收集出来
17.4 把所有循环强行改成 Stream
Stream 适合筛选、转换、聚合。复杂状态机、需要多次 break/continue、逐步调试的流程,普通循环可能更清楚。可读性比“代码短”更重要。
17.5 盲目使用 parallelStream
并行流不是自动加速按钮。小数据、阻塞 I/O、共享可变状态、顺序敏感任务可能更慢或出错。零基础阶段先正确掌握顺序流,后续并发课程再深入并行流。
18. 完整基础案例:LambdaAndStreamBasics
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class LambdaAndStreamBasics {
public static void main(String[] args) {
// Predicate:接收一个 Integer,判断是否及格。
Predicate<Integer> passed = score -> score >= 60;
// Function:把 String 转换为 Integer,也就是字符串长度。
Function<String, Integer> length = String::length;
// Consumer:接收字符串并打印,不返回结果。
Consumer<String> printer = text -> System.out.println("消息:" + text);
// Supplier:没有参数,需要时创建一个新的 ArrayList。
Supplier<List<String>> listFactory = ArrayList::new;
System.out.println("75 分是否及格:" + passed.test(75));
System.out.println("Java 的长度:" + length.apply("Java"));
printer.accept("开始学习 Stream");
System.out.println("新列表:" + listFactory.get());
List<String> skills = List.of("Java", "SQL", "Git", "Spring", "Java");
List<String> result = skills.stream()
.distinct() // 去掉重复 Java
.filter(skill -> skill.length() >= 4) // 只保留长度至少 4 的技能
.sorted(Comparator.comparingInt(String::length)) // 按长度升序
.map(String::toUpperCase) // 转成大写
.toList(); // 收集为新 List
System.out.println("处理结果:" + result);
System.out.println("原始数据:" + skills);
}
}
样例输出:
75 分是否及格:true
Java 的长度:4
消息:开始学习 Stream
新列表:[]
处理结果:[JAVA, SPRING]
原始数据:[Java, SQL, Git, Spring, Java]
为什么结果只有 [JAVA, SPRING]:
distinct()去掉第二个 Java。filter删除长度小于 4 的 SQL 和 Git。- Java 长度 4,Spring 长度 6,所以排序后 Java 在前。
map把两个字符串转换成大写。- 原始
skills没有被 Stream 修改。
19. 综合案例:员工数据报表
需求
公司希望从员工列表中得到:
- 在职员工姓名,按工资从高到低排列。
- Java 部门的平均工资。
- 按部门分组的员工姓名。
- 是否存在工资低于 6000 的在职员工。
完整代码
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class EmployeeReport {
// record 用来保存一条员工数据;Java 会自动生成访问方法。
record Employee(String name, String department, double salary, boolean active) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("小明", "Java", 8500, true),
new Employee("小红", "测试", 7200, true),
new Employee("小李", "Java", 9500, true),
new Employee("小王", "Java", 6000, false),
new Employee("小赵", "运维", 5800, true)
);
// 报表一:筛选在职员工 -> 按工资降序 -> 只取姓名 -> 收集成 List。
List<String> activeNames = employees.stream()
.filter(Employee::active) // 等价于 employee -> employee.active()
.sorted(Comparator.comparingDouble(Employee::salary).reversed())
.map(Employee::name) // Employee 转换成 String
.toList();
// 报表二:筛选 Java 部门 -> 提取 double 工资 -> 求平均值。
double javaAverageSalary = employees.stream()
.filter(employee -> employee.department().equals("Java"))
.mapToDouble(Employee::salary)
.average()
.orElse(0.0); // 如果没有 Java 员工,平均工资使用 0
// 报表三:以部门为 key,把同一部门的姓名收集到 List 中。
Map<String, List<String>> namesByDepartment = employees.stream()
.collect(Collectors.groupingBy(
Employee::department, // 分组 key
Collectors.mapping( // 分组后不要整个 Employee,只要姓名
Employee::name,
Collectors.toList()
)
));
// 报表四:找到第一个满足条件的元素后即可停止。
boolean hasLowPaidActiveEmployee = employees.stream()
.anyMatch(employee -> employee.active() && employee.salary() < 6000);
System.out.println("在职员工(工资降序):" + activeNames);
System.out.printf("Java 部门平均工资:%.2f%n", javaAverageSalary);
System.out.println("部门员工:" + namesByDepartment);
System.out.println("是否有工资低于 6000 的在职员工:" + hasLowPaidActiveEmployee);
}
}
样例输出
HashMap 分组结果的部门显示顺序不应作为业务承诺,但每组内容如下:
在职员工(工资降序):[小李, 小明, 小红, 小赵]
Java 部门平均工资:8000.00
部门员工:{Java=[小明, 小李, 小王], 测试=[小红], 运维=[小赵]}
是否有工资低于 6000 的在职员工:true
为什么 Java 平均工资是 8000
Java 部门有三人,包括离职的小王,因为需求只说“Java 部门”,没有要求“在职”:
(8500 + 9500 + 6000) / 3 = 8000
如果需求改为“在职 Java 员工平均工资”,需要再加一个过滤条件:
.filter(Employee::active)
.filter(employee -> employee.department().equals("Java"))
这体现了 Stream 的特点:一条管道就是一份清楚的数据处理需求。
20. 独立练习(先想,再看答案)
给定:
List<Integer> scores = List.of(88, 45, 92, 60, 88, 73);
要求:
- 筛选及格成绩。
- 去重。
- 从高到低排序。
- 收集成新 List。
- 计算及格成绩的平均分。
参考答案
List<Integer> passedScores = scores.stream()
.filter(score -> score >= 60) // 留下 88、92、60、88、73
.distinct() // 去掉重复的 88
.sorted(Comparator.reverseOrder()) // 从高到低
.toList();
double average = scores.stream()
.filter(score -> score >= 60) // 平均分通常保留原始重复记录
.mapToInt(Integer::intValue) // 转成 IntStream
.average()
.orElse(0.0);
System.out.println("及格成绩:" + passedScores);
System.out.printf("及格平均分:%.2f%n", average);
样例输出:
及格成绩:[92, 88, 73, 60]
及格平均分:80.20
平均分计算保留两个 88:
(88 + 92 + 60 + 88 + 73) / 5 = 80.2
21. 检查题与答案
问题
- Lambda 能脱离函数式接口独立存在吗?
- 函数式接口最多有几个抽象方法?
Predicate<T>的输入和输出是什么?Function<T,R>中 T 和 R 分别表示什么?Consumer<T>为什么没有返回值?Supplier<T>有几个输入参数?String::length对应什么 Lambda?- Stream 和 Collection 的核心职责有什么区别?
- 中间操作为什么称为惰性操作?
filter和map的区别是什么?flatMap解决什么问题?reduce(0, Integer::sum)中的 0 是什么?- 为什么
max()返回 Optional? - 同一个 Stream 能执行两次终止操作吗?
groupingBy和partitioningBy有什么区别?toMap遇到重复 key 可能发生什么?- 为什么不推荐在 Stream 中修改外部 List?
- 是否应该把所有 for 循环改成 Stream?
- 为什么零基础阶段不直接使用
parallelStream()? mapToInt有什么作用?
答案
- 不能;它需要目标函数式接口类型。
- 一个,但可以有默认方法和静态方法。
- 输入 T,输出 boolean。
- T 是输入类型,R 是返回类型。
- 它表示消费数据并执行动作,抽象方法
accept返回 void。 - 零个,通过
get()产生结果。 text -> text.length()。- Collection 负责存储和访问数据,Stream 负责描述和执行数据处理管道。
- 没有终止操作时,中间操作通常不会真正遍历数据。
- filter 决定元素是否保留;map 把元素转换成另一个值或类型。
- 把多层 Stream 或集合摊平成一层。
- 恒等值,也是累加器的初始结果。
- 空 Stream 不存在最大值。
- 不能;Stream 通常只能消费一次。
- groupingBy 可以产生任意多个分组,partitioningBy 固定按 true/false 分两组。
- 默认抛
IllegalStateException,应提供合并规则。 - 会产生副作用,使结果依赖外部状态,在并行处理时还可能出现数据竞争。
- 不应该;复杂状态流程有时普通循环更清楚。
- 并行有调度成本,还要求无共享可变状态,并不保证更快。
- 把对象流转换为
IntStream,便于直接使用 sum、average 等数值操作。
22. 本课完成标准
- 能把匿名内部类的单方法实现改写为 Lambda。
- 能根据输入、输出选择 Predicate、Function、Consumer、Supplier。
- 能把方法引用还原为普通 Lambda。
- 能解释 Stream 的惰性执行和一次消费。
- 能区分 filter、map、flatMap、reduce、collect。
- 能安全处理 Optional。
- 能完成筛选、排序、转换、分组、统计。
- 能解释综合案例的每一条管道和样例输出。
- 能识别副作用、重复消费、重复 key 和盲目并行等错误。
23. 参考资料
- Dev.java:Lambda Expressions
- Dev.java:Functional Interfaces
- Dev.java:Method References
- Dev.java:The Stream API
- Dev.java:Map / Filter / R…
- Java 17 Stream API
- OpenJDK JDK 源码仓库
下一课将学习文件 I/O、Path/Files、字符编码与 try-with-resources,并继续采用“原理、逐行注释、样例输出、常见错误、综合案例”的标准。