Stream 综合案例 | JavaSE

Stream 综合案例

一、学习目标

完成本章后,你应该能够:

  • 能够把一个真实的数据处理需求拆解为完整的 Stream Pipeline。
  • 能够综合使用 filter()map()sorted()distinct()limit() 等中间操作。
  • 能够综合使用 count()findFirst()anyMatch()allMatch()max()reduce() 等终结操作。
  • 能够把 Stream 处理结果收集为 ListSetMap 和数组。
  • 能够根据业务需求选择 Lambda、方法引用和 Comparator。
  • 能够分析 Stream 每一步的数据类型变化。
  • 能够判断 Stream 操作的合理顺序,而不是机械堆叠 API。
  • 能够使用 Stream 完成一个完整的学生成绩分析程序。

二、案例需求

本章不再学习新的核心 Stream 语法,而是把前面已经学习的内容组合起来。

我们设计一个:

学生成绩分析系统。

系统中保存若干学生:

学生编号
姓名
年龄
城市
成绩

现在需要完成以下功能。

需求一:查询所有及格学生

条件:

score >= 60

要求按照成绩从高到低排序,并得到:

List<Student>

需求二:查询前三名学生姓名

要求:

全部学生
↓
成绩降序
↓
前 3 名
↓
提取姓名
↓
List<String>

需求三:统计及格人数

最终得到:

long

需求四:查询最高分学生

最终得到:

Optional<Student>

并正确处理不存在学生的情况。


需求五:判断是否存在满分学生

最终得到:

boolean

需求六:判断是否所有学生都及格

最终得到:

boolean

需求七:获取所有学生所在城市

要求:

城市不能重复。

最终得到:

Set<String>

需求八:建立学生索引

按照:

学生 id → Student

建立:

Map<Long, Student>

需求九:计算所有学生总成绩和平均成绩

综合使用:

map
+
reduce

完成总成绩计算,再求平均成绩。


需求十:按照城市对学生分组

得到:

Map<String, List<Student>>

例如:

北京
→ 张三、赵敏

上海
→ 李四

太原
→ 王五、周芷若

三、准备数据模型

3.1 Student 类

public class Student {

    private long id;
    private String name;
    private int age;
    private String city;
    private double score;

    public Student(
            long id,
            String name,
            int age,
            String city,
            double score
    ) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.city = city;
        this.score = score;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getCity() {
        return city;
    }

    public double getScore() {
        return score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", city='" + city + '\'' +
                ", score=" + score +
                '}';
    }
}

3.2 准备学生集合

List<Student> students = List.of(
        new Student(
                1001,
                "张无忌",
                20,
                "北京",
                96.5
        ),
        new Student(
                1002,
                "赵敏",
                19,
                "北京",
                91.0
        ),
        new Student(
                1003,
                "周芷若",
                20,
                "上海",
                88.5
        ),
        new Student(
                1004,
                "张三丰",
                21,
                "太原",
                59.0
        ),
        new Student(
                1005,
                "小昭",
                18,
                "上海",
                76.0
        ),
        new Student(
                1006,
                "谢逊",
                22,
                "太原",
                100.0
        )
);

现在原始数据模型是:

List<Student>

后续所有 Stream 操作都围绕:

students.stream()

展开。


四、案例一:查询所有及格学生

4.1 需求分析

需求:

找出成绩大于等于 60 的学生,并按照成绩从高到低排列。

先不要急着写代码。

应该先拆流水线:

students
   ↓
获得 Stream
   ↓
过滤 score >= 60
   ↓
成绩降序
   ↓
收集 List

转换成 Stream API:

stream
↓
filter
↓
sorted
↓
toList

4.2 实现

List<Student> passedStudents =
        students.stream()
                .filter(
                        student ->
                                student.getScore() >= 60
                )
                .sorted(
                        Comparator
                                .comparingDouble(
                                        Student::getScore
                                )
                                .reversed()
                )
                .toList();

4.3 类型分析

第一步:

students.stream()

得到:

Stream<Student>

经过:

filter(...)

仍然:

Stream<Student>

因为:

filter() 只是决定 Student 留不留下,并不会改变元素类型。

经过:

sorted(...)

仍然:

Stream<Student>

最后:

toList()

得到:

List<Student>

完整类型链:

List<Student>
     ↓
Stream<Student>
     ↓ filter
Stream<Student>
     ↓ sorted
Stream<Student>
     ↓ toList
List<Student>

五、案例二:查询前三名学生姓名

5.1 需求拆解

需求:

找到成绩最高的前三名,只返回他们的姓名。

正确顺序应该是:

全部学生
   ↓
按成绩降序
   ↓
取前三名
   ↓
Student 转 String
   ↓
收集 List

因此:

sorted
↓
limit
↓
map
↓
toList

5.2 实现

List<String> topThreeNames =
        students.stream()
                .sorted(
                        Comparator
                                .comparingDouble(
                                        Student::getScore
                                )
                                .reversed()
                )
                .limit(3)
                .map(
                        Student::getName
                )
                .toList();

5.3 最关键的 map

在:

.map(
        Student::getName
)

之前:

Stream<Student>

之后:

Stream<String>

因为:

Student
   ↓
getName()
   ↓
String

这就是 map() 的真正价值:

把一种元素转换为另一种元素。


5.4 为什么不能先 limit

下面写法:

students.stream()
        .limit(3)
        .sorted(
                Comparator
                        .comparingDouble(
                                Student::getScore
                        )
                        .reversed()
        );

表示的是:

先拿原集合前三个人
↓
再对这三个人排序

它并不能得到:

全体学生前三名。

正确:

sorted(...)
.limit(3)

所以:

Stream API 的调用顺序,就是业务规则本身。


六、案例三:统计及格人数

需求:

成绩 >= 60
↓
一共有多少人?

代码:

long passedCount =
        students.stream()
                .filter(
                        student ->
                                student.getScore() >= 60
                )
                .count();

结构:

Stream<Student>
     ↓ filter
Stream<Student>
     ↓ count
long

这里:

count()

是终结操作。

流水线执行到这里结束。


七、案例四:查询最高分学生

7.1 max

代码:

Optional<Student> topStudent =
        students.stream()
                .max(
                        Comparator
                                .comparingDouble(
                                        Student::getScore
                                )
                );

这里:

Comparator.comparingDouble(
        Student::getScore
)

定义:

成绩越大,Student 越大。

所以:

max(...)

得到最高分学生。


7.2 为什么不是 Student

返回:

Optional<Student>

而不是:

Student

原因是:

如果 Stream 为空
↓
根本不存在最高分学生

因此结果存在:

有值
或
没有值

两种状态。


7.3 安全处理

例如:

topStudent.ifPresent(
        student ->
                System.out.println(
                        "最高分学生:"
                                + student
                )
);

如果业务规定:

学生列表不能为空。

可以:

Student student =
        topStudent.orElseThrow(
                () ->
                        new IllegalStateException(
                                "学生数据为空"
                        )
        );

八、案例五:判断是否存在满分学生

需求:

是否至少存在一个成绩等于 100 的学生?

非常适合:

anyMatch()

代码:

boolean hasPerfectScore =
        students.stream()
                .anyMatch(
                        student ->
                                student.getScore() == 100
                );

自然语言:

任何一个学生
是否满足
score == 100?

因此:

anyMatch
=
至少一个满足

8.1 为什么不应该先 filter 再 count

当然也可以:

long count =
        students.stream()
                .filter(
                        student ->
                                student.getScore() == 100
                )
                .count();

boolean result =
        count > 0;

但需求本身只是:

有没有?

使用:

anyMatch(...)

更加直接。

并且 anyMatch() 是短路终结操作:

找到第一个满足条件的元素后,就已经能够确定结果。


九、案例六:判断是否所有学生及格

使用:

allMatch()

代码:

boolean allPassed =
        students.stream()
                .allMatch(
                        student ->
                                student.getScore() >= 60
                );

可以直接读:

所有学生是否都满足成绩大于等于 60?


9.1 anyMatch / allMatch / noneMatch

回顾:

anyMatch
至少一个满足?

allMatch
全部满足?

noneMatch
一个都不满足?

例如检查:

是否没有学生低于 60 分?

还可以:

boolean nobodyFailed =
        students.stream()
                .noneMatch(
                        student ->
                                student.getScore() < 60
                );

在业务表达上:

allMatch(score >= 60)

和:

noneMatch(score < 60)

表达的结果逻辑接近。

应该选择:

更符合业务语言、更加容易阅读的写法。


十、案例七:获取所有不同城市

需求:

学生
↓
取城市
↓
去重

一种写法:

Set<String> cities =
        students.stream()
                .map(
                        Student::getCity
                )
                .collect(
                        Collectors.toSet()
                );

过程:

Stream<Student>
      ↓ map
Stream<String>
      ↓ collect
Set<String>

因为最终使用:

Set

重复城市自然不会保留。


10.1 另一种思路

也可以:

List<String> cities =
        students.stream()
                .map(
                        Student::getCity
                )
                .distinct()
                .toList();

区别是:

第一种:
结果数据模型就是 Set

第二种:
Pipeline 中先 distinct
最终结果仍然是 List

应该根据:

最终业务需要什么数据结构

决定。


十一、案例八:建立 id → Student Map

需求:

学生 id
↓
对应 Student

最终:

Map<Long, Student>

可以:

Map<Long, Student> studentMap =
        students.stream()
                .collect(
                        Collectors.toMap(
                                Student::getId,
                                Function.identity()
                        )
                );

11.1 Function.identity()

这里:

Function.identity()

可以理解为:

student -> student

因此:

Student::getId

负责生成:

key

而:

Function.identity()

负责生成:

value

完整逻辑:

Student
   ├── id ─────→ key
   │
   └── Student → value

11.2 为什么 id 应该唯一

如果两个 Student 的:

id

相同:

Collectors.toMap(...)

会产生键冲突。

因此作为:

Student 唯一标识

id 应保证业务唯一。

如果业务数据确实可能重复,则必须显式提供合并策略。

例如保留后出现的数据:

Map<Long, Student> studentMap =
        students.stream()
                .collect(
                        Collectors.toMap(
                                Student::getId,
                                Function.identity(),
                                (oldStudent, newStudent) ->
                                        newStudent
                        )
                );

十二、案例九:计算总成绩与平均成绩

12.1 总成绩

本阶段已经学习 reduce()

可以:

double totalScore =
        students.stream()
                .map(
                        Student::getScore
                )
                .reduce(
                        0.0,
                        Double::sum
                );

过程:

Student
   ↓ map(getScore)
Double
   ↓ reduce
double 总成绩

12.2 reduce 的逻辑

假设成绩:

96.5
91.0
88.5

归约:

0.0 + 96.5
    ↓
96.5 + 91.0
    ↓
187.5 + 88.5
    ↓
276.0

所以:

reduce(
        0.0,
        Double::sum
)

表达:

把所有成绩不断累加成一个结果。


12.3 平均成绩

有:

double totalScore

和:

students.size()

可以:

double averageScore =
        students.isEmpty()
                ? 0.0
                : totalScore
                        / students.size();

这里首先处理空集合,避免:

没有学生时平均成绩失去合理业务含义。


12.4 Java 数值 Stream 的另一种能力

JDK 21 Stream API 还提供基本类型 Stream。

例如:

students.stream()
        .mapToDouble(
                Student::getScore
        )

得到:

DoubleStream

它可以直接:

sum()
average()
max()
min()

因此实际代码还可以:

double total =
        students.stream()
                .mapToDouble(
                        Student::getScore
                )
                .sum();

平均值:

OptionalDouble average =
        students.stream()
                .mapToDouble(
                        Student::getScore
                )
                .average();

这里属于对前面 Stream 能力的自然扩展。

本章主线仍然以已经学习过的:

map()
+
reduce()

为主。


十三、案例十:按照城市分组

13.1 需求

希望得到:

Map<String, List<Student>>

例如:

北京
→ [张无忌, 赵敏]

上海
→ [周芷若, 小昭]

太原
→ [张三丰, 谢逊]

这种需求本质不是:

一个元素 → 一个 key/value

而是:

按照某个分类条件,把多个元素分到不同组中。


13.2 groupingBy

可以:

Map<String, List<Student>> studentsByCity =
        students.stream()
                .collect(
                        Collectors.groupingBy(
                                Student::getCity
                        )
                );

这里:

Student::getCity

叫分类函数。

每个 Student:

Student
  ↓
getCity()
  ↓
分类 key

具有相同 city 的 Student 会进入同一个 List。


13.3 groupingBy 与 toMap 的区别

这是非常重要的综合判断。

toMap() 更适合:

一个唯一 key
→
一个 value

例如:

studentId → Student

groupingBy() 适合:

一个分类 key
→
多个元素

例如:

city → List<Student>

所以:

id → Student
使用 toMap

city → List<Student>
使用 groupingBy

不要看到需要 Map 就全部使用:

Collectors.toMap(...)

十四、把所有案例组织到一个程序中

下面把上述需求组合起来。

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

public class StreamComprehensiveDemo {

    public static void main(String[] args) {

        List<Student> students = List.of(
                new Student(
                        1001,
                        "张无忌",
                        20,
                        "北京",
                        96.5
                ),
                new Student(
                        1002,
                        "赵敏",
                        19,
                        "北京",
                        91.0
                ),
                new Student(
                        1003,
                        "周芷若",
                        20,
                        "上海",
                        88.5
                ),
                new Student(
                        1004,
                        "张三丰",
                        21,
                        "太原",
                        59.0
                ),
                new Student(
                        1005,
                        "小昭",
                        18,
                        "上海",
                        76.0
                ),
                new Student(
                        1006,
                        "谢逊",
                        22,
                        "太原",
                        100.0
                )
        );

        // 1. 所有及格学生,成绩降序
        List<Student> passedStudents =
                students.stream()
                        .filter(
                                student ->
                                        student.getScore()
                                                >= 60
                        )
                        .sorted(
                                Comparator
                                        .comparingDouble(
                                                Student::getScore
                                        )
                                        .reversed()
                        )
                        .toList();

        System.out.println(
                "及格学生:" + passedStudents
        );

        // 2. 前三名姓名
        List<String> topThreeNames =
                students.stream()
                        .sorted(
                                Comparator
                                        .comparingDouble(
                                                Student::getScore
                                        )
                                        .reversed()
                        )
                        .limit(3)
                        .map(
                                Student::getName
                        )
                        .toList();

        System.out.println(
                "前三名:" + topThreeNames
        );

        // 3. 及格人数
        long passedCount =
                students.stream()
                        .filter(
                                student ->
                                        student.getScore()
                                                >= 60
                        )
                        .count();

        System.out.println(
                "及格人数:" + passedCount
        );

        // 4. 最高分学生
        Optional<Student> topStudent =
                students.stream()
                        .max(
                                Comparator
                                        .comparingDouble(
                                                Student::getScore
                                        )
                        );

        topStudent.ifPresent(
                student ->
                        System.out.println(
                                "最高分学生:"
                                        + student
                        )
        );

        // 5. 是否存在满分
        boolean hasPerfectScore =
                students.stream()
                        .anyMatch(
                                student ->
                                        student.getScore()
                                                == 100
                        );

        System.out.println(
                "是否存在满分学生:"
                        + hasPerfectScore
        );

        // 6. 是否全部及格
        boolean allPassed =
                students.stream()
                        .allMatch(
                                student ->
                                        student.getScore()
                                                >= 60
                        );

        System.out.println(
                "是否全部及格:"
                        + allPassed
        );

        // 7. 所有不同城市
        Set<String> cities =
                students.stream()
                        .map(
                                Student::getCity
                        )
                        .collect(
                                Collectors.toSet()
                        );

        System.out.println(
                "城市:" + cities
        );

        // 8. id -> Student
        Map<Long, Student> studentMap =
                students.stream()
                        .collect(
                                Collectors.toMap(
                                        Student::getId,
                                        Function.identity()
                                )
                        );

        System.out.println(
                "学生索引:" + studentMap
        );

        // 9. 总成绩
        double totalScore =
                students.stream()
                        .map(
                                Student::getScore
                        )
                        .reduce(
                                0.0,
                                Double::sum
                        );

        System.out.println(
                "总成绩:" + totalScore
        );

        double averageScore =
                students.isEmpty()
                        ? 0.0
                        : totalScore
                                / students.size();

        System.out.println(
                "平均成绩:" + averageScore
        );

        // 10. 按城市分组
        Map<String, List<Student>>
                studentsByCity =
                students.stream()
                        .collect(
                                Collectors.groupingBy(
                                        Student::getCity
                                )
                        );

        System.out.println(
                "城市分组:" + studentsByCity
        );
    }
}

Student:

public class Student {

    private long id;
    private String name;
    private int age;
    private String city;
    private double score;

    public Student(
            long id,
            String name,
            int age,
            String city,
            double score
    ) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.city = city;
        this.score = score;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getCity() {
        return city;
    }

    public double getScore() {
        return score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", city='" + city + '\'' +
                ", score=" + score +
                '}';
    }
}

十五、如何真正设计一条 Stream Pipeline

综合案例真正需要掌握的不是:

背住上面那几十行代码。

而是建立一种固定的分析流程。

第一步:确定数据源

例如:

List<Student>

第二步:明确最终结果

例如:

我要前三名学生姓名。

最终:

List<String>

而不是:

List<Student>

第三步:用自然语言写业务步骤

学生
↓
成绩降序
↓
前三名
↓
姓名
↓
List

第四步:把自然语言映射成 Stream API

学生
↓
stream()

成绩降序
↓
sorted()

前三名
↓
limit()

姓名
↓
map()

List
↓
toList()

第五步:检查每一步类型

List<Student>

↓ stream

Stream<Student>

↓ sorted

Stream<Student>

↓ limit

Stream<Student>

↓ map(Student::getName)

Stream<String>

↓ toList

List<String>

如果每一步类型都能够解释清楚,Stream 代码通常就不会乱。


十六、Stream API 的选择模型

综合起来,可以建立一张非常重要的“需求 → API”映射。

| 业务需求 | Stream API | | -------------------- | ------------------------- | | 只保留满足条件的数据 | filter() | | 把 A 转成 B | map() | | 去重 | distinct() | | 排序 | sorted() | | 取前 N 个 | limit() | | 跳过前 N 个 | skip() | | 遍历处理 | forEach() | | 统计数量 | count() | | 最大值 | max() | | 最小值 | min() | | 找第一个 | findFirst() | | 是否至少一个满足 | anyMatch() | | 是否全部满足 | allMatch() | | 是否全部不满足 | noneMatch() | | 多个值归并为一个值 | reduce() | | 收集为 List | toList() / collect() | | 收集为 Set | Collectors.toSet() | | 建立 key-value | Collectors.toMap() | | 按条件分组 | Collectors.groupingBy() | | 收集数组 | toArray() |

最终不应该背:

filter map sorted limit...

而应该形成:

看到需求
↓
立即想到对应的数据操作

十七、一个非常重要的原则:先过滤,再做昂贵操作

例如:

students.stream()
        .sorted(...)
        .filter(
                student ->
                        student.getScore() >= 60
        );

如果业务目标只是:

对及格学生排序。

更加合理:

students.stream()
        .filter(
                student ->
                        student.getScore() >= 60
        )
        .sorted(...);

原因很直观:

10000 个学生
↓
先 filter
↓
5000 个及格

再排序 5000 个

通常比:

10000 个全部排序
↓
最后只留下 5000 个

更加合理。

所以 Stream Pipeline 不只是:

API 顺序语法正确。

还需要考虑:

业务语义和处理成本。


十八、不要在 Stream 中制造大量副作用

例如:

students.stream()
        .filter(student -> {
            database.save(student);

            return student.getScore() >= 60;
        });

这种设计非常不好理解。

filter() 应该表达:

是否保留。

而不是偷偷承担:

数据库写入
日志更新
外部状态修改

Stream 的函数式风格强调:

操作应尽量是无干扰(non-interfering)的,并避免依赖可变共享状态。

因此:

filter

就负责过滤。

map

就负责转换。

sorted

就负责排序。

不要把大量不相关副作用塞进去。


十九、Stream 不一定比 for 循环更合适

例如:

for (int i = 0;
     i < students.size();
     i++) {

    System.out.println(
            i + ":"
                    + students.get(i)
    );
}

这里业务天然依赖:

索引 i

传统 for 循环可能非常直观。

不要为了:

“我已经学了 Stream”

而强迫所有代码变成:

stream()

Stream 特别擅长:

筛选
映射
排序
聚合
查找
分组
收集

但:

工具选择的标准始终是清晰、正确和适合业务。


二十、常见问题

20.1 Stream 综合题应该从 API 开始想吗?

不应该。

错误思路:

这个题是不是要用 filter?
是不是还要 map?
是不是还缺个 sorted?

正确思路:

数据是什么?

最终结果是什么?

中间要经过哪些业务变化?

这些变化分别对应什么 API?

20.2 filter 和 map 谁先谁后?

取决于数据类型和业务条件。

例如:

students.stream()
        .filter(
                student ->
                        student.getScore() >= 60
        )
        .map(
                Student::getName
        );

合理。

如果先:

map(
        Student::getName
)

之后 Stream 已经变成:

Stream<String>

就无法再写:

student.getScore()

所以必须随时知道:

当前 Stream 元素是什么类型。


20.3 找最高分为什么不一定需要 sorted?

如果只想:

找一个最高分学生。

直接:

max(...)

更加符合语义。

而:

sorted(...)
.findFirst()

通常意味着完成排序后再取元素,表达更加绕。


20.4 去重应该用 distinct 还是 Set?

取决于后续需求。

仍要继续流水线:

distinct()

已经准备结束并且最终业务结果就是集合去重:

Collectors.toSet()

更自然。


20.5 toMap 为什么必须考虑 key 唯一性?

因为:

Map

一个 key 只能对应一个最终 value。

如果多个 Stream 元素映射成同一个 key,就必须明确:

保留旧值?
保留新值?
求最大值?
求和?
其他合并规则?

20.6 Stream.toList() 能直接 add 吗?

JDK 21 中:

stream.toList()

返回不可修改 List。

如果业务后续明确需要:

add()
remove()

应该创建合适的可修改集合,例如:

students.stream()
        .collect(
                Collectors.toCollection(
                        ArrayList::new
                )
        );

20.7 groupingBy 与 toMap 怎么选择?

可以先问:

同一个 key
应该只有一个 value?

还是可能对应很多元素?

如果:

id → Student

使用:

toMap()

如果:

city → 多个 Student

使用:

groupingBy()

二十一、练习与验收

21.1 知识问答

  1. Stream 综合题应该从什么开始分析?
  2. 为什么必须明确最终结果类型?
  3. filter()map() 的根本区别是什么?
  4. 为什么 Stream 每一步的元素类型都需要关注?
  5. 为什么找最大值通常应该直接使用 max()
  6. 为什么判断“是否存在”通常使用 anyMatch()filter().count() 更直接?
  7. toMap()groupingBy() 的业务模型分别是什么?
  8. 为什么 Stream 中不应该大量依赖副作用?
  9. 为什么操作顺序不仅影响结果,也可能影响处理成本?
  10. Stream 是否应该完全替代传统循环?

21.2 代码阅读

阅读:

List<String> result =
        students.stream()
                .filter(
                        student ->
                                student.getScore()
                                        >= 60
                )
                .sorted(
                        Comparator
                                .comparingDouble(
                                        Student::getScore
                                )
                                .reversed()
                )
                .limit(3)
                .map(
                        Student::getName
                )
                .toList();

回答:

  1. filter 前后元素类型是什么?
  2. sorted 前后元素类型是什么?
  3. limit 前后元素类型是什么?
  4. map 前后元素类型是什么?
  5. 最终 result 是什么类型?
  6. 整条代码的自然语言需求是什么?

分析:

students.stream()
        .map(
                Student::getName
        )
        .filter(
                student ->
                        student.getScore() >= 60
        );

这段代码为什么无法成立?


21.3 手写代码

给定:

List<Student> students;

闭卷完成:

  1. 查询全部及格学生。
  2. 按成绩从高到低排序。
  3. 获取前 3 名。
  4. 获取前 3 名姓名。
  5. 统计不及格人数。
  6. 查询年龄最大的学生。
  7. 判断是否存在未成年学生。
  8. 判断是否所有学生成绩大于等于 60。
  9. 获取所有不同城市。
  10. 建立 id → Student Map。
  11. 计算总成绩。
  12. 按城市分组。

21.4 Debug

业务目标:

获取所有及格学生中成绩最高的前三名姓名。

代码:

students.stream()
        .limit(3)
        .map(
                Student::getName
        )
        .filter(
                student ->
                        student.getScore() >= 60
        )
        .sorted();

请至少指出:

  • 操作顺序问题;
  • Stream 元素类型问题;
  • 排序规则问题;
  • 最终结果收集问题。

并重新设计完整 Pipeline。


21.5 综合训练

假设星雨笔录后台存在:

List<Article> articles;

Article:

id
title
category
viewCount
published
author

要求不查看本章示例,自行设计:

  1. 找出所有已经发布的 Java 文章。
  2. 按浏览量降序排列。
  3. 获取浏览量最高的前 10 篇。
  4. 只保留标题。
  5. 收集为 List<String>
  6. 统计已经发布的文章数量。
  7. 判断是否存在浏览量超过 100000 的文章。
  8. 找浏览量最高的文章。
  9. 获取所有文章分类并去重。
  10. 建立 id → Article Map。
  11. 按 category 对文章分组。
  12. 计算所有文章总浏览量。

每一道题都先写:

数据源
↓
业务步骤
↓
Stream API
↓
最终结果类型

然后再写代码。


21.6 本章验收

如果给你:

List<Student> students;

你应该能够不查看任何资料,根据需求自行组合:

stream
filter
map
sorted
distinct
skip
limit
max
min
findFirst
anyMatch
allMatch
noneMatch
count
reduce
toList
toSet
toMap
groupingBy
toArray

但真正的验收标准不是:

能背出这些 API。

而是能够面对:

“查询及格学生中成绩最高的前三名姓名”

立即拆出:

students

↓ stream

Stream<Student>

↓ filter(score >= 60)

Stream<Student>

↓ sorted(score DESC)

Stream<Student>

↓ limit(3)

Stream<Student>

↓ map(getName)

Stream<String>

↓ toList()

List<String>

如果你能够:

  1. 写出正确业务顺序;
  2. 说清楚每一步元素类型;
  3. 判断哪些是中间操作;
  4. 判断哪个是终结操作;
  5. 解释 Lambda / 方法引用的目标函数式接口;
  6. 根据业务正确选择 List、Set、Map 或单值结果;
  7. Debug 操作顺序和类型错误;

那么就可以认为:

Stream 主线已经真正形成体系,而不是停留在 API 记忆阶段。