Stream API 基础
本章定位:Stream API 是 JDK 8 中最具生产力的特性,它让 Java 开发者可以用声明式、函数式的方式处理集合数据。本章将深入 Stream 的创建、中间操作、终端操作,以及最重要的特性——惰性求值。
1. 场景引入:孔蓝的困惑
飞翔科技的校招生孔蓝第一天入职就被分配了一个任务:从公司 500 名员工数据中,找出技术部薪资最高的 3 名 90 后员工。她用 JDK 7 的写法折腾了半天:
// JDK 7:近 20 行代码,可读性差
List<Employee> techEmps = new ArrayList<>();
for (Employee e : employees) {
if ("技术部".equals(e.getDepartment())) {
techEmps.add(e);
}
}
Collections.sort(techEmps, new Comparator<Employee>() {
@Override
public int compare(Employee a, Employee b) {
return Double.compare(b.getSalary(), a.getSalary());
}
});
List<Employee> top3 = new ArrayList<>();
for (int i = 0; i < Math.min(3, techEmps.size()); i++) {
top3.add(techEmps.get(i));
}
架构师白歌 review 代码时叹了口气,写下了 JDK 8 版本:
// JDK 8:一行流式处理,语义清晰
List<Employee> top3 = employees.stream()
.filter(e -> "技术部".equals(e.getDepartment()))
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(3)
.collect(Collectors.toList());
孔蓝看呆了——这就是 Stream API 的威力。
2. Stream 的概念
2.1 什么是 Stream
Stream(流) 不是数据结构,不存储数据。它是对数据源(集合、数组、I/O 通道等)的函数式操作管道,用于对数据进行聚合、转换和计算。
| 特性 | 说明 | 类比 |
|---|---|---|
| 不存储数据 | Stream 只是一个"视图",操作完后数据仍在原集合中 | 像 SQL 的视图,不存储数据 |
| 函数式 | 操作通过 Lambda 传递行为,不修改数据源 | 类似 RxJava / React 的响应式流 |
| 惰性求值 | 中间操作不会立即执行,只有终端操作触发时才执行 | 像 Builder 模式,build() 时才真正构建 |
| 一次性 | Stream 只能被消费一次,消费完后需重新创建 | 像 Iterator,遍历完就没了 |
| 内部迭代 | 迭代逻辑由 Stream 库内部管理,开发者只声明"做什么" | 数据库执行计划 |
2.2 Stream vs 集合
3. Stream 的创建
3.1 创建方式总览
| 创建方式 | 语法 | 适用场景 |
|---|---|---|
| 从集合 | collection.stream() | 最常用,List/Set 等 |
| 从数组 | Arrays.stream(array) | 基本类型数组、对象数组 |
| 直接创建 | Stream.of(T... values) | 少量已知元素 |
| 无限流(迭代) | Stream.iterate(seed, unaryOp) | 数学序列、无限序列 |
| 无限流(生成) | Stream.generate(supplier) | 随机数、常量流 |
| 从文件 | Files.lines(path) | 按行读取文件 |
| 从字符串 | "abc".chars() | 字符处理 |
| Builder | Stream.builder().add().build() | 动态构建 |
3.2 代码示例
// 1. 从集合创建(最常用)
List<Employee> list = getEmployees();
Stream<Employee> stream1 = list.stream(); // 顺序流
Stream<Employee> parallelStream = list.parallelStream(); // 并行流
// 2. 从数组创建
String[] names = {"大翔", "白歌", "小崔", "孔蓝"};
Stream<String> stream2 = Arrays.stream(names);
Stream<String> subStream = Arrays.stream(names, 1, 3); // 只取 [白歌, 小崔]
// 3. 直接创建
Stream<Integer> stream3 = Stream.of(1, 2, 3, 4, 5);
Stream<String> stream4 = Stream.of("A", "B", "C");
// 4. 无限流(需要用 limit() 截断)
Stream<Integer> naturalNumbers = Stream.iterate(0, n -> n + 1); // 0, 1, 2, 3, ...
Stream<Double> randomValues = Stream.generate(Math::random); // 随机数流
// 5. 从文件读取
try (Stream<String> lines = Files.lines(Paths.get("employees.txt"))) {
lines.filter(line -> line.contains("技术部"))
.forEach(System.out::println);
}
注意:
Stream.iterate()和Stream.generate()创建的无限流必须配合limit()使用,否则会无限执行下去。
4. 中间操作(Intermediate Operations)
中间操作返回一个新的 Stream,多个中间操作可以链式调用形成管道。中间操作不会立即执行,它们只是记录下操作步骤,等终端操作触发时才真正执行——这称为惰性求值。
4.1 中间操作速查表
| 操作 | 方法签名 | 功能 | 是否有状态 |
|---|---|---|---|
| filter | Stream<T> filter(Predicate<T>) | 按条件过滤 | 无状态 |
| map | <R> Stream<R> map(Function<T,R>) | 元素一对一转换 | 无状态 |
| flatMap | <R> Stream<R> flatMap(Function<T,Stream<R>>) | 元素一对多展平 | 无状态 |
| distinct | Stream<T> distinct() | 去重(基于 equals) | 有状态 |
| sorted | Stream<T> sorted() / sorted(Comparator) | 排序 | 有状态 |
| peek | Stream<T> peek(Consumer<T>) | 调试/窥视(不改变元素) | 无状态 |
| limit | Stream<T> limit(long n) | 截取前 n 个 | 有状态(短路) |
| skip | Stream<T> skip(long n) | 跳过前 n 个 | 有状态 |
4.2 filter:过滤
// 筛选技术部薪资 > 15000 的员工
List<Employee> techHighSalary = employees.stream()
.filter(e -> "技术部".equals(e.getDepartment()))
.filter(e -> e.getSalary() > 15000)
.collect(Collectors.toList());
4.3 map:转换
// 提取所有员工的姓名列表
List<String> names = employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());
// 计算每个员工的年薪
List<Double> annualSalaries = employees.stream()
.map(emp -> emp.getSalary() * 12)
.collect(Collectors.toList());
4.4 flatMap:展平
flatMap 将每个元素映射为一个 Stream,然后将所有 Stream 连接成一个 Stream——即"展平"操作。
// 场景:每个员工有多个技能标签,获取所有技能的去重列表
class Employee {
private List<String> skills; // ["Java", "Spring", ...]
public List<String> getSkills() { return skills; }
}
// 错误做法:map 得到的是 Stream<List<String>>
Stream<List<String>> skillLists = employees.stream().map(Employee::getSkills);
// 正确做法:flatMap 得到 Stream<String>
List<String> allSkills = employees.stream()
.flatMap(emp -> emp.getSkills().stream())
.distinct()
.collect(Collectors.toList());
4.5 sorted:排序
// 按薪资降序排列
List<Employee> bySalary = employees.stream()
.sorted((a, b) -> Double.compare(b.getSalary(), a.getSalary()))
.collect(Collectors.toList());
// 使用 Comparator 工具方法(推荐)
List<Employee> bySalary2 = employees.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.collect(Collectors.toList());
// 多级排序:先按部门,再按薪资
List<Employee> multiSort = employees.stream()
.sorted(Comparator.comparing(Employee::getDepartment)
.thenComparing(Comparator.comparingDouble(Employee::getSalary).reversed()))
.collect(Collectors.toList());
4.6 peek:调试利器
// 使用 peek 观察管道中每一步的数据变化
List<String> result = employees.stream()
.peek(e -> System.out.println("原始: " + e))
.filter(e -> e.getSalary() > 15000)
.peek(e -> System.out.println(" 过滤后: " + e))
.map(Employee::getName)
.peek(name -> System.out.println(" 映射后: " + name))
.collect(Collectors.toList());
注意:peek 主要用于调试,不应依赖其副作用。在并行流中,peek 的执行顺序可能是非确定性的。
4.7 limit / skip:分页与截取
// 获取薪资最高的前 3 名
List<Employee> top3 = employees.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(3)
.collect(Collectors.toList());
// 分页:第 2 页,每页 10 条
int page = 2;
int pageSize = 10;
List<Employee> page2 = employees.stream()
.skip((page - 1) * pageSize)
.limit(pageSize)
.collect(Collectors.toList());
5. 终端操作(Terminal Operations)
终端操作会触发 Stream 管道的执行,返回最终结果(非 Stream 类型)。执行后,Stream 被消费,不能再次使用。
5.1 终端操作速查表
| 操作 | 返回类型 | 功能 | 短路? |
|---|---|---|---|
| forEach | void | 遍历每个元素 | 否 |
| collect | R(集合/Map/字符串等) | 将流收集为集合 | 否 |
| toArray | Object[] / T[] | 转换为数组 | 否 |
| reduce | Optional<T> / T | 归约/聚合 | 否 |
| count | long | 计数 | 否 |
| anyMatch | boolean | 存在任一匹配 | 是 |
| allMatch | boolean | 全部匹配 | 是 |
| noneMatch | boolean | 无匹配 | 是 |
| findFirst | Optional<T> | 找第一个 | 是 |
| findAny | Optional<T> | 找任意一个(并行友好) | 是 |
| min/max | Optional<T> | 最小/最大值 | 否 |
5.2 短路操作(Short-Circuiting)
短路操作是指不需要处理完整个流就能得出结果的操作。一旦找到满足条件的元素,立即终止后续元素的处理。
短路操作的强大在于:配合 limit(),可以避免不必要的计算。例如从无限流中取前 10 个元素:
// 生成前 100 个斐波那契数对
Stream.iterate(new long[]{0, 1}, f -> new long[]{f[1], f[0] + f[1]})
.limit(100) // 短路:只处理 100 个
.map(f -> f[0])
.forEach(System.out::println);
// findFirst 短路示例
Optional<Employee> firstHighSalary = employees.stream()
.filter(e -> e.getSalary() > 50000)
.findFirst(); // 找到第一个就停止,不会处理后续元素
6. 惰性求值详解
6.1 什么是惰性求值
惰性求值(Lazy Evaluation) 是指中间操作不会立即执行,而是等到终端操作被调用时,才一次性"拉取"数据并执行整个管道。这是 Stream 性能优势的核心。
6.2 惰性求值实验
public class LazyEvaluationDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("大翔", "白歌", "小崔", "孔蓝");
System.out.println("=== 实验1:无终端操作 ===");
Stream<String> stream = names.stream()
.filter(name -> {
System.out.println(" filter 正在处理: " + name);
return name.length() > 1;
})
.map(name -> {
System.out.println(" map 正在处理: " + name);
return name.toUpperCase();
});
System.out.println("Stream 管道已构建,但没有任何输出!");
System.out.println("\n=== 实验2:添加终端操作后 ===");
// 此时才真正执行
List<String> result = stream.collect(Collectors.toList());
System.out.println("结果: " + result);
}
}
运行输出:
=== 实验1:无终端操作 ===
Stream 管道已构建,但没有任何输出!
=== 实验2:添加终端操作后 ===
filter 正在处理: 大翔
map 正在处理: 大翔
filter 正在处理: 白歌
map 正在处理: 白歌
filter 正在处理: 小崔
map 正在处理: 小崔
filter 正在处理: 孔蓝
map 正在处理: 孔蓝
结果: [大翔, 白歌, 小崔, 孔蓝]
关键发现:终端操作 collect 调用之前,filter 和 map 的代码根本没有执行!
6.3 惰性求值的执行模型
惰性求值带来的优势:
- 减少遍历次数:不需要为每个中间操作创建临时集合,一次遍历完成所有操作
- 短路优化:
limit(3)配合sorted()可能只需找到"最小的 3 个"而无需完整排序 - 无限流支持:可以在无限流上使用
limit()安全截断 - 内存友好:中间结果不会存储在临时集合中(有状态操作如 sorted 除外)
7. 完整代码示例
示例一:飞翔科技员工综合查询
场景:小崔需要实现一个员工综合查询功能,支持按部门、薪资范围、入职时间进行筛选,结果按薪资排序,支持分页。
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class EmployeeQueryDemo {
public static void main(String[] args) {
// 准备测试数据
List<Employee> employees = Arrays.asList(
new Employee("大翔", 50000, "管理层", LocalDate.of(2015, 3, 1),
Arrays.asList("管理", "战略", "Java")),
new Employee("白歌", 35000, "技术部", LocalDate.of(2017, 6, 15),
Arrays.asList("Java", "Spring", "架构", "分布式")),
new Employee("小崔", 18000, "技术部", LocalDate.of(2021, 7, 1),
Arrays.asList("Java", "MyBatis", "SQL")),
new Employee("孔蓝", 12000, "技术部", LocalDate.of(2023, 9, 1),
Arrays.asList("Python", "AI", "Django")),
new Employee("老刘", 9000, "后勤部", LocalDate.of(2018, 1, 10),
Arrays.asList("采购", "物流")),
new Employee("阿杰", 25000, "产品部", LocalDate.of(2019, 4, 20),
Arrays.asList("需求分析", "Axure", "数据分析")),
new Employee("小美", 15000, "产品部", LocalDate.of(2022, 11, 5),
Arrays.asList("UI设计", "Figma", "用户体验"))
);
System.out.println("=== 1. 技术部薪资 > 15000 的员工,按薪资降序 ===");
employees.stream()
.filter(e -> "技术部".equals(e.getDepartment()))
.filter(e -> e.getSalary() > 15000)
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.forEach(e -> System.out.printf(" %s, 薪资: %.0f%n", e.getName(), e.getSalary()));
System.out.println("\n=== 2. 各部门的员工名单(flatMap 展平技能) ===");
Map<String, List<String>> deptEmployees = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));
deptEmployees.forEach((dept, names) ->
System.out.printf(" %s: %s%n", dept, names));
System.out.println("\n=== 3. 所有技能去重列表 ===");
List<String> allSkills = employees.stream()
.flatMap(e -> e.getSkills().stream())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(" " + allSkills);
System.out.println("\n=== 4. 薪资分页(第1页,每页3条) ===");
List<Employee> page1 = employees.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(3)
.collect(Collectors.toList());
page1.forEach(e -> System.out.printf(" %s: %.0f%n", e.getName(), e.getSalary()));
System.out.println("\n=== 5. 统计信息 ===");
long techCount = employees.stream()
.filter(e -> "技术部".equals(e.getDepartment()))
.count();
boolean hasHighSalary = employees.stream()
.anyMatch(e -> e.getSalary() > 40000);
boolean allHaveName = employees.stream()
.allMatch(e -> e.getName() != null && !e.getName().isEmpty());
System.out.printf(" 技术部人数: %d%n", techCount);
System.out.printf(" 存在薪资 > 40000: %b%n", hasHighSalary);
System.out.printf(" 所有员工都有姓名: %b%n", allHaveName);
System.out.println("\n=== 6. 首个薪资最低的员工 ===");
employees.stream()
.min(Comparator.comparingDouble(Employee::getSalary))
.ifPresent(e -> System.out.printf(" 最低薪资: %s, %.0f%n", e.getName(), e.getSalary()));
}
}
运行输出:
=== 1. 技术部薪资 > 15000 的员工,按薪资降序 ===
白歌, 薪资: 35000
小崔, 薪资: 18000
=== 2. 各部门的员工名单(flatMap 展平技能) ===
技术部: [白歌, 小崔, 孔蓝]
产品部: [阿杰, 小美]
后勤部: [老刘]
管理层: [大翔]
=== 3. 所有技能去重列表 ===
[AI, Axure, Django, Figma, Java, MyBatis, Python, SQL, Spring, UI设计, 分布式, 战略, 数据分析, 架构, 物流, 用户体验, 管理, 采购, 需求分析]
=== 4. 薪资分页(第1页,每页3条) ===
大翔: 50000
白歌: 35000
阿杰: 25000
=== 5. 统计信息 ===
技术部人数: 3
存在薪资 > 40000: true
所有员工都有姓名: true
=== 6. 首个薪资最低的员工 ===
最低薪资: 老刘, 9000
示例二:无限流与数据生成
场景:白歌需要生成一批测试用的员工 ID,格式为 "EMP-YYYY-NNNN"(年份 + 序号)。
import java.util.stream.Stream;
public class InfiniteStreamDemo {
public static void main(String[] args) {
// 生成员工编号:EMP-2024-0001 格式
System.out.println("=== 生成前 10 个员工编号 ===");
Stream.iterate(1, n -> n + 1)
.limit(10)
.map(n -> String.format("EMP-2024-%04d", n))
.forEach(System.out::println);
// 斐波那契数列
System.out.println("\n=== 前 10 个斐波那契数 ===");
Stream.iterate(new long[]{0, 1}, f -> new long[]{f[1], f[0] + f[1]})
.limit(10)
.map(f -> f[0])
.forEach(n -> System.out.print(n + " "));
System.out.println();
// 生成随机薪资数据(18000 ~ 50000)
System.out.println("\n=== 生成 5 个随机薪资 ===");
Stream.generate(() -> Math.random() * 32000 + 18000)
.limit(5)
.map(salary -> Math.round(salary * 100) / 100.0)
.forEach(salary -> System.out.printf(" %.2f%n", salary));
// 验证短路:在无限流中找第一个满足条件的
System.out.println("\n=== 在无限流中找第一个 > 45000 的值 ===");
Stream.generate(() -> Math.random() * 32000 + 18000)
.peek(v -> System.out.printf(" 检查: %.2f%n", v))
.filter(v -> v > 45000)
.findFirst()
.ifPresent(v -> System.out.printf("找到! %.2f%n", v));
}
}
运行输出:
=== 生成前 10 个员工编号 ===
EMP-2024-0001
EMP-2024-0002
EMP-2024-0003
EMP-2024-0004
EMP-2024-0005
EMP-2024-0006
EMP-2024-0007
EMP-2024-0008
EMP-2024-0009
EMP-2024-0010
=== 前 10 个斐波那契数 ===
0 1 1 2 3 5 8 13 21 34
=== 生成 5 个随机薪资 ===
27654.32
19456.78
45123.90
22109.45
38976.11
=== 在无限流中找第一个 > 45000 的值 ===
检查: 23012.45
检查: 34219.87
检查: 46201.33
找到! 46201.33
8. 易错场景
反例一:Stream 重复消费
小崔不小心对一个 Stream 调用了两次终端操作:
// ❌ 错误:Stream 只能消费一次
Stream<Employee> stream = employees.stream().filter(e -> e.getSalary() > 15000);
List<Employee> list1 = stream.collect(Collectors.toList()); // 第一次消费 OK
List<Employee> list2 = stream.collect(Collectors.toList()); // IllegalStateException
报错信息:
java.lang.IllegalStateException: stream has already been operated upon or closed
// ✅ 正确:使用 Supplier 包装,每次需要时重新创建
Supplier<Stream<Employee>> streamSupplier = () ->
employees.stream().filter(e -> e.getSalary() > 15000);
List<Employee> list1 = streamSupplier.get().collect(Collectors.toList());
List<Employee> list2 = streamSupplier.get().collect(Collectors.toList());
反例二:惰性求值陷阱——副作用在中间操作中
// ❌ 错误:期望 filter 中的打印语句执行,但忘了加终端操作
employees.stream()
.filter(e -> {
System.out.println("处理: " + e.getName()); // 这行永远不会执行!
return e.getSalary() > 15000;
});
System.out.println("完成");
// 输出只有 "完成",没有员工信息
// ✅ 正确:添加终端操作触发执行
employees.stream()
.filter(e -> {
System.out.println("处理: " + e.getName());
return e.getSalary() > 15000;
})
.collect(Collectors.toList()); // 必须有终端操作
反例三:在 Stream 操作中修改外部集合
// ❌ 错误:在 filter 的 Lambda 中修改外部集合——副作用 + 非线程安全
List<Employee> highSalary = new ArrayList<>();
employees.stream()
.filter(e -> {
if (e.getSalary() > 15000) {
highSalary.add(e); // 副作用!不应在 Stream 操作中修改外部状态
}
return true;
})
.collect(Collectors.toList());
// ✅ 正确:使用 collect 收集结果
List<Employee> highSalary = employees.stream()
.filter(e -> e.getSalary() > 15000)
.collect(Collectors.toList());
反例四:sorted + limit 的性能误解
// ❌ 错误认知:以为 sorted() 会完整排序所有元素
// 实际上 JDK 的 sorted() 实现确实会完整排序(对于 Collection.stream()),
// 但如果数据量很大,limit(3) 无法帮助 sorted() 减少工作量
employees.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(3) // limit 只是从排序结果中截取,排序仍然处理了所有元素
.collect(Collectors.toList());
// ✅ 优化:如果只需要 Top N,考虑使用优先队列(PriorityQueue)
// 或者在使用并行流时,sorted 的实现会更高效
// 对于极大数据集,手动实现 Top N 算法比 sorted().limit(N) 更高效
9. 面试考点
Q1:Stream 和集合有什么区别?
- 存储:集合是数据结构,存储数据;Stream 不存储数据,只是数据管道的抽象。
- 修改:集合可以增删改元素;Stream 不修改数据源,只产生结果。
- 迭代:集合是外部迭代(for-each 由调用者控制);Stream 是内部迭代(由库控制,支持并行优化)。
- 遍历:集合可多次遍历;Stream 只能消费一次。
- 求值:集合是立即的(eager);Stream 的中间操作是惰性的(lazy)。
Q2:什么是惰性求值?有什么好处?
惰性求值是指中间操作不会立即执行,而是等到终端操作触发时才一次性执行。好处:
- 减少遍历次数:多个操作合并为一次遍历
- 支持短路:
findFirst()+filter()找到第一个匹配项就停止- 支持无限流:可以在无限流上用
limit()安全截断- 内存友好:不产生中间临时集合
Q3:中间操作和终端操作的区别是什么?
中间操作返回
Stream类型,可以链式调用,且是惰性的(不触发执行);终端操作返回非Stream的结果(void、集合、Optional、boolean 等),调用后会触发整个管道的执行并消费 Stream。常见中间操作:filter、map、sorted。常见终端操作:collect、forEach、reduce、findFirst。
Q4:flatMap 和 map 的区别?
map是一对一映射,将 T 转换为 R;flatMap是一对多映射 + 展平,将 T 转换为Stream<R>然后将多个流合并为一个流。典型场景:将嵌套集合展平(如List<List<String>>→List<String>),或处理 Optional(Optional.flatMap())。
Q5:short-circuiting(短路)操作有哪些?
limit(n):截取前 n 个后停止处理后续元素findFirst()/findAny():找到第一个/任意一个匹配项后停止anyMatch()/allMatch()/noneMatch():匹配到足以判断结果后停止短路操作配合惰性求值,可以大幅减少不必要的元素处理。
白歌总结:"Stream API 让 Java 开发者第一次拥有了媲美 SQL 的数据操作表达能力。记住三个核心原则:不修改数据源、惰性求值、内部迭代。掌握了这三点,你就真正理解了 Stream。"