第 13 课:Lambda、函数式接口、方法引用与 Stream

3 阅读18分钟

第 13 课:Lambda、函数式接口、方法引用与 Stream

适用环境:Java 17。本文按零基础编写,所有例子都说明“为什么写、每行做什么、结果是什么”。

本课目标

学完后,你应该能够:

  1. 解释 Lambda 为什么出现,而不只是背 ->
  2. 判断一个接口能不能接收 Lambda。
  3. 正确使用 PredicateFunctionConsumerSupplier
  4. 看懂四种方法引用,并能还原成 Lambda。
  5. 区分集合和 Stream,理解惰性执行与一次消费。
  6. 使用 filtermapsortedreducecollect 处理数据。
  7. 看懂 Optional,避免随意调用 get()
  8. 完成一个员工数据统计综合案例,并能解释样例输出。

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

执行过程:

  1. add 的类型是 Calculator
  2. 编译器查看 Calculator.calculate(int, int),知道 Lambda 必须接收两个整数并返回整数。
  3. 调用 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>:接收两个参数。
  • IntPredicateIntFunction<R>IntConsumer:基本类型专用版本,减少装箱。

7. 方法引用:给已有方法起“快捷入口”

当 Lambda 只是把参数原样交给一个现有方法时,可以使用 ::

Consumer<String> lambda = text -> System.out.println(text);
Consumer<String> reference = System.out::println;

两者行为相同。方法引用不是“立即调用方法”,而是把调用方式保存起来,等 accept() 执行时才调用。

四种方法引用

类型方法引用对应 Lambda
静态方法Integer::parseInttext -> Integer.parseInt(text)
特定对象的实例方法System.out::printlntext -> System.out.println(text)
任意对象的实例方法String::lengthtext -> 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.comparingIntInteger.compare 更安全、更清楚。


9. Stream 是什么,不是什么

集合负责存储数据;Stream 负责描述怎样处理数据。Stream 自己不存储元素。

集合/数组等数据源
      ↓
创建 Stream
      ↓
中间操作:filtermapsorted……
      ↓
终止操作: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 的三个核心特征:

  1. 惰性执行:没有终止操作时,中间操作通常不会真正处理数据。
  2. 一次消费:终止操作后,不能再次使用同一个 Stream。
  3. 不修改数据源:规范用法是产生新结果,不在管道里修改原集合。

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); // 2Stream.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]

  1. distinct() 去掉第二个 Java。
  2. filter 删除长度小于 4 的 SQL 和 Git。
  3. Java 长度 4,Spring 长度 6,所以排序后 Java 在前。
  4. map 把两个字符串转换成大写。
  5. 原始 skills 没有被 Stream 修改。

19. 综合案例:员工数据报表

需求

公司希望从员工列表中得到:

  1. 在职员工姓名,按工资从高到低排列。
  2. Java 部门的平均工资。
  3. 按部门分组的员工姓名。
  4. 是否存在工资低于 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);

要求:

  1. 筛选及格成绩。
  2. 去重。
  3. 从高到低排序。
  4. 收集成新 List。
  5. 计算及格成绩的平均分。

参考答案

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. 检查题与答案

问题

  1. Lambda 能脱离函数式接口独立存在吗?
  2. 函数式接口最多有几个抽象方法?
  3. Predicate<T> 的输入和输出是什么?
  4. Function<T,R> 中 T 和 R 分别表示什么?
  5. Consumer<T> 为什么没有返回值?
  6. Supplier<T> 有几个输入参数?
  7. String::length 对应什么 Lambda?
  8. Stream 和 Collection 的核心职责有什么区别?
  9. 中间操作为什么称为惰性操作?
  10. filtermap 的区别是什么?
  11. flatMap 解决什么问题?
  12. reduce(0, Integer::sum) 中的 0 是什么?
  13. 为什么 max() 返回 Optional?
  14. 同一个 Stream 能执行两次终止操作吗?
  15. groupingBypartitioningBy 有什么区别?
  16. toMap 遇到重复 key 可能发生什么?
  17. 为什么不推荐在 Stream 中修改外部 List?
  18. 是否应该把所有 for 循环改成 Stream?
  19. 为什么零基础阶段不直接使用 parallelStream()
  20. mapToInt 有什么作用?

答案

  1. 不能;它需要目标函数式接口类型。
  2. 一个,但可以有默认方法和静态方法。
  3. 输入 T,输出 boolean。
  4. T 是输入类型,R 是返回类型。
  5. 它表示消费数据并执行动作,抽象方法 accept 返回 void。
  6. 零个,通过 get() 产生结果。
  7. text -> text.length()
  8. Collection 负责存储和访问数据,Stream 负责描述和执行数据处理管道。
  9. 没有终止操作时,中间操作通常不会真正遍历数据。
  10. filter 决定元素是否保留;map 把元素转换成另一个值或类型。
  11. 把多层 Stream 或集合摊平成一层。
  12. 恒等值,也是累加器的初始结果。
  13. 空 Stream 不存在最大值。
  14. 不能;Stream 通常只能消费一次。
  15. groupingBy 可以产生任意多个分组,partitioningBy 固定按 true/false 分两组。
  16. 默认抛 IllegalStateException,应提供合并规则。
  17. 会产生副作用,使结果依赖外部状态,在并行处理时还可能出现数据竞争。
  18. 不应该;复杂状态流程有时普通循环更清楚。
  19. 并行有调度成本,还要求无共享可变状态,并不保证更快。
  20. 把对象流转换为 IntStream,便于直接使用 sum、average 等数值操作。

22. 本课完成标准

  • 能把匿名内部类的单方法实现改写为 Lambda。
  • 能根据输入、输出选择 Predicate、Function、Consumer、Supplier。
  • 能把方法引用还原为普通 Lambda。
  • 能解释 Stream 的惰性执行和一次消费。
  • 能区分 filter、map、flatMap、reduce、collect。
  • 能安全处理 Optional。
  • 能完成筛选、排序、转换、分组、统计。
  • 能解释综合案例的每一条管道和样例输出。
  • 能识别副作用、重复消费、重复 key 和盲目并行等错误。

23. 参考资料

下一课将学习文件 I/O、Path/Files、字符编码与 try-with-resources,并继续采用“原理、逐行注释、样例输出、常见错误、综合案例”的标准。