Java 8 引入的 Stream API 是 Java 函数式编程的核心利器。配合 Lambda 表达式,可以用声明式的方式处理集合数据,代码更简洁、更易读,还能轻松实现并行计算。本文从入门到实战,带你全面掌握 Stream。
一、Stream 是什么
1.1 概念
Stream(流)是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
特点:
- Stream 自己不存储元素,只是对数据进行计算
- Stream 操作不会改变源数据,每次操作返回新的 Stream
- 操作是延迟执行的,只有触发终止操作时才会真正执行
- 流只能被消费一次,遍历完就用完了
1.2 操作分类
Stream 操作
├── 中间操作(Intermediate)—— 返回新 Stream,延迟执行
│ ├── 无状态:filter、map、flatMap、peek
│ └── 有状态:distinct、sorted、limit、skip
└── 终止操作(Terminal)—— 触发计算,返回最终结果
├── 非短路:forEach、collect、reduce、count
└── 短路:anyMatch、allMatch、findFirst、findAny
二、创建 Stream 的四种方式
2.1 通过集合创建(最常用)
List<String> list = Arrays.asList("a", "b", "c");
// 顺序流
Stream<String> stream = list.stream();
// 并行流
Stream<String> parallelStream = list.parallelStream();
2.2 通过数组创建
String[] array = {"apple", "banana", "cherry"};
Stream<String> stream = Arrays.stream(array);
// 指定范围
Stream<String> partStream = Arrays.stream(array, 0, 2);
2.3 通过 Stream.of
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
2.4 创建无限流
// 迭代:从0开始,每次+2
Stream<Integer> iterate = Stream.iterate(0, x -> x + 2);
// 生成:随机数
Stream<Double> generate = Stream.generate(Math::random);
三、中间操作
3.1 filter — 过滤
筛选符合条件的元素:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 筛选偶数
List<Integer> evens = list.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// 结果:[2, 4, 6, 8, 10]
3.2 map — 映射转换
将元素转换为另一种形式:
List<String> names = Arrays.asList("zhangsan", "lisi", "wangwu");
// 转大写
List<String> upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// 结果:[ZHANGSAN, LISI, WANGWU]
// 获取字符串长度
List<Integer> lengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
// 结果:[8, 4, 6]
3.3 flatMap — 扁平化映射
将流中每个元素都换成另一个流,然后把所有流合成一个流:
List<String> words = Arrays.asList("hello", "world");
// 拆分字符(map 会得到 Stream<Stream<String>>)
List<Stream<String>> result1 = words.stream()
.map(word -> word.split(""))
.map(Arrays::stream)
.collect(Collectors.toList());
// flatMap 扁平化(得到 Stream<String>)
List<String> result2 = words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.collect(Collectors.toList());
// 结果:[h, e, l, l, o, w, o, r, l, d]
3.4 distinct — 去重
List<Integer> list = Arrays.asList(1, 2, 2, 3, 3, 3, 4);
List<Integer> unique = list.stream()
.distinct()
.collect(Collectors.toList());
// 结果:[1, 2, 3, 4]
3.5 sorted — 排序
List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);
// 自然排序
List<Integer> sorted1 = list.stream()
.sorted()
.collect(Collectors.toList());
// 自定义排序(倒序)
List<Integer> sorted2 = list.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
// 对象按字段排序
List<Employee> sortedEmp = employees.stream()
.sorted(Comparator.comparing(Employee::getAge))
.collect(Collectors.toList());
3.6 limit & skip — 截断与跳过
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 取前3个
List<Integer> limit = list.stream()
.limit(3)
.collect(Collectors.toList());
// [1, 2, 3]
// 跳过前5个
List<Integer> skip = list.stream()
.skip(5)
.collect(Collectors.toList());
// [6, 7, 8, 9, 10]
// 分页:第2页,每页3条
List<Integer> page = list.stream()
.skip(3)
.limit(3)
.collect(Collectors.toList());
// [4, 5, 6]
3.7 peek — 调试用
和 forEach 类似,但属于中间操作,常用于调试:
List<Integer> result = Stream.of(1, 2, 3)
.peek(n -> System.out.println("原数据: " + n))
.map(n -> n * 2)
.peek(n -> System.out.println("翻倍后: " + n))
.collect(Collectors.toList());
四、终止操作
4.1 forEach — 遍历
list.stream().forEach(System.out::println);
4.2 count — 计数
long count = list.stream().count();
4.3 max / min — 最值
// 最大值
Optional<Integer> max = list.stream().max(Integer::compareTo);
// 员工中年龄最大的
Optional<Employee> oldest = employees.stream()
.max(Comparator.comparing(Employee::getAge));
4.4 findFirst / findAny — 查找
// 找第一个
Optional<Integer> first = list.stream().findFirst();
// 找任意一个(并行流中效率更高)
Optional<Integer> any = list.parallelStream().findAny();
4.5 匹配操作
// 是否全部满足
boolean allEven = list.stream().allMatch(n -> n % 2 == 0);
// 是否有一个满足
boolean hasEven = list.stream().anyMatch(n -> n % 2 == 0);
// 是否全部不满足
boolean noneNegative = list.stream().noneMatch(n -> n < 0);
4.6 reduce — 归约
将流中元素反复结合,得到一个值:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
// 求和(有初始值)
Integer sum = list.stream().reduce(0, Integer::sum);
// 结果:15
// 求最大值(无初始值,返回 Optional)
Optional<Integer> max = list.stream().reduce(Integer::max);
// 拼接字符串
List<String> words = Arrays.asList("Hello", "Stream", "World");
String joined = words.stream().reduce("", (a, b) -> a + " " + b);
// 结果:" Hello Stream World"
五、Collectors 收集器(重点)
collect 是最常用的终止操作,配合 Collectors 工具类可以实现各种收集需求。
5.1 转集合
// 转 List
List<String> list = stream.collect(Collectors.toList());
// 转 Set
Set<String> set = stream.collect(Collectors.toSet());
// 转指定集合类型
LinkedList<String> linkedList = stream.collect(Collectors.toCollection(LinkedList::new));
5.2 转 Map
// id -> 对象 的映射
Map<Long, Employee> empMap = employees.stream()
.collect(Collectors.toMap(
Employee::getId, // key
Function.identity(), // value(对象本身)
(v1, v2) -> v1 // key 冲突时的处理策略
));
// id -> name 的映射
Map<Long, String> idNameMap = employees.stream()
.collect(Collectors.toMap(Employee::getId, Employee::getName));
5.3 分组 groupingBy
按某个字段分组,是开发中最常用的操作之一:
// 按部门分组
Map<String, List<Employee>> deptMap = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
// 多级分组:先按部门,再按年龄
Map<String, Map<String, List<Employee>>> multiGroup = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(emp ->
emp.getAge() > 30 ? "中年" : "青年"
)
));
// 分组并统计数量
Map<String, Long> deptCount = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));
// 分组并求平均值
Map<String, Double> deptAvgSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
5.4 分区 partitioningBy
特殊的分组,只有 true/false 两组:
// 按薪资是否大于10000分区
Map<Boolean, List<Employee>> partition = employees.stream()
.collect(Collectors.partitioningBy(emp -> emp.getSalary() > 10000));
// true 是高薪组,false 是普通组
List<Employee> highSalary = partition.get(true);
List<Employee> normalSalary = partition.get(false);
5.5 聚合统计
// 统计数量、总和、最小值、平均值、最大值
IntSummaryStatistics stats = employees.stream()
.collect(Collectors.summarizingInt(Employee::getAge));
stats.getCount(); // 总数
stats.getSum(); // 总和
stats.getMin(); // 最小值
stats.getAverage(); // 平均值
stats.getMax(); // 最大值
5.6 拼接字符串
// 用逗号拼接所有名字
String names = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
// 带前后缀
String result = words.stream()
.collect(Collectors.joining(", ", "[", "]"));
// 结果:[Hello, Stream, World]
六、实战案例
案例一:从员工列表中筛选统计
// 需求:找出研发部门薪资大于8000的员工名字,按薪资倒序,取前3个
List<String> top3Names = employees.stream()
.filter(emp -> "研发部".equals(emp.getDepartment()))
.filter(emp -> emp.getSalary() > 8000)
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.limit(3)
.map(Employee::getName)
.collect(Collectors.toList());
案例二:计算每个部门的薪资总和
Map<String, Double> deptTotalSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.summingDouble(Employee::getSalary)
));
案例三:嵌套集合扁平化
// 一个班级有多个学生,每个学生有多个爱好。找出所有爱好去重
List<String> allHobbies = classes.stream()
.flatMap(c -> c.getStudents().stream())
.flatMap(s -> s.getHobbies().stream())
.distinct()
.collect(Collectors.toList());
案例四:List 转树形结构
// 把扁平的菜单列表转成树形结构
List<Menu> menuTree = menuList.stream()
.filter(menu -> menu.getParentId() == 0) // 顶级菜单
.peek(menu -> menu.setChildren(getChildren(menu, menuList)))
.collect(Collectors.toList());
// 递归查找子菜单(实际开发中可用 Map 优化)
private List<Menu> getChildren(Menu parent, List<Menu> all) {
return all.stream()
.filter(menu -> menu.getParentId().equals(parent.getId()))
.peek(menu -> menu.setChildren(getChildren(menu, all)))
.collect(Collectors.toList());
}
七、并行流
7.1 使用方式
// 直接获取并行流
list.parallelStream()
// 普通流转并行
list.stream().parallel()
7.2 适用场景
- 数据量大、计算密集型任务
- 每个元素计算独立,无状态依赖
- CPU 多核能发挥优势
7.3 注意事项
// ❌ 错误:并行流操作共享变量,结果不确定
int[] sum = {0};
list.parallelStream().forEach(n -> sum[0] += n);
// ✅ 正确:使用 reduce 或 collect
Integer sum = list.parallelStream().reduce(0, Integer::sum);
- 并行流默认使用
ForkJoinPool.commonPool(),全局共享 - I/O 密集型任务不适合并行流(线程阻塞反而浪费资源)
- 小数据量不要用并行流,线程切换开销大于收益
八、Optional 配合使用
Stream 很多操作返回 Optional,配合使用能优雅避免空指针:
// 找到第一个薪资大于10000的员工,获取名字
String name = employees.stream()
.filter(emp -> emp.getSalary() > 10000)
.findFirst()
.map(Employee::getName)
.orElse("未找到");
Stream 是 Java 8 最实用的特性之一,熟练使用能大幅提升代码质量。但也要注意:不要为了用 Stream 而强行写复杂的一行流,可读性永远是第一位的。复杂逻辑拆成几步,比一行难以理解的长链更利于维护。