270. Java Stream API - 从"怎么做"转向"要什么结果":声明式编程的优势
🎯 目标任务
我们要计算一个城市列表中,人口超过 10 万的城市总人口数。
🧱 传统命令式写法(Java 代码)
java
record City(int population) {}
List<City> cities = List.of(
new City(100_000),
new City(200_000),
new City(500_000)
);
int sum = 0;
for (City city : cities) {
int population = city.population();
if (population > 100_000) {
sum += population;
}
}
System.out.println("Sum = " + sum); // 输出:700000
这段代码很好地完成了目标,但是命令式的------一步一步告诉程序要做什么。
💡 思维实验:如果 Collection 有 map 和 filter 会怎样?
假设我们扩展 Collection 接口,给它添加 map() 和 filter() 方法,并且它们返回的是 Collection:
java
Collection<Integer> populations = cities.map(city -> city.population());
Collection<Integer> filteredPopulations = populations.filter(p -> p > 100_000);
int sum = filteredPopulations.sum();
看起来很"链式",很优雅,但有一个严重问题 :每一步都要创建中间集合!
❌ 性能陷阱:中间集合的隐性开销
map()会遍历所有城市,并创建一个新的集合保存每个城市的人口数。filter()会再遍历这个人口集合,选出符合条件的。sum()会再遍历过滤结果做加总。
👉 如果处理 上百万城市对象 ,那么这会造成大量 内存分配和垃圾回收压力。
而传统的 for 循环是一边遍历、一边判断、一边累加的,没有任何中间结构的创建。
✅ 为什么是 Stream,而不是 Collection?
🌊 Stream 的关键特性:不存储数据,只描述处理过程
java
int sum = cities.stream()
.mapToInt(City::getPopulation) // 先映射成人口数
.filter(p -> p > 100_000) // 筛选出人口超 10 万的
.sum(); // 聚合求和(终端操作)
在这段代码中:
- 没有中间集合产生;
.mapToInt()和.filter()都是中间操作,只是"排管道";.sum()是终端操作,才真正触发数据流动和计算。
🧠 懒加载(Lazy Evaluation):终端操作才"开水龙头"
Stream就像"工厂流水线",每个操作(map/filter)都是一个加工环节;- 只有当你调用
.sum()、.collect()等终端操作时,才真的开始处理每个数据; - 每个数据只"走一次管道":
💡 不是"先映射完所有人口" ➝ "再筛选" ➝ "再加总", 而是:每个城市 ➝ 映射 ➝ 判断是否保留 ➝ 累加(即一条龙服务🚀)。
⚙️ 示例:短路操作节省时间
需求:判断是否存在人口超过 100_000 的城市
传统 Collection 风格必须遍历:
java
boolean exists = cities.map(City::population)
.filter(p -> p > 100_000)
.anyMatch(p -> true); // 已经浪费了两步处理
而用 Stream 可以做到只看第一个符合条件的城市:
java
boolean exists = cities.stream()
.anyMatch(c -> c.getPopulation() > 100_000);
💥 一旦遇到满足条件的城市,就立即返回 true,后续不再处理!
📊 总结:为什么 map/filter 不属于 Collection 接口?
| 方式 | map/filter 返回类型 | 是否创建中间集合 | 是否惰性求值 | 是否支持短路 |
|---|---|---|---|---|
Collection.map() |
Collection | ✅ 会创建 | ❌ 否 | ❌ 否 |
Stream.map() |
Stream | ❌ 不创建 | ✅ 是 | ✅ 是(如 anyMatch) |
👉 正因为Collection 是数据容器 ,而 Stream 是操作流水线 ,所以 map() 和 filter() 被设计在 Stream 而非 Collection。
🧵 Stream API 的流水线模型:终端操作才"触发机器运转"
| 操作类型 | 举例 | 描述 |
|---|---|---|
| 中间操作 | map(), filter(), sorted() |
返回 Stream,不触发计算 |
| 终端操作 | sum(), collect(), forEach() |
返回非 Stream,触发计算 |
📘 实操建议(练习题):
- 使用
stream()实现:计算人口超过 200_000 城市的平均人口; - 统计有多少个城市符合人口 > 100_000;
- 判断是否所有城市人口都 > 50_000。