Java 8 Stream API 实战指南,告别繁琐循环

Java 8 引入的 Stream API 是 Java 函数式编程的核心利器。配合 Lambda 表达式,可以用声明式的方式处理集合数据,代码更简洁、更易读,还能轻松实现并行计算。本文从入门到实战,带你全面掌握 Stream。

一、Stream 是什么

1.1 概念

Stream(流)是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

特点

  • Stream 自己不存储元素,只是对数据进行计算
  • Stream 操作不会改变源数据,每次操作返回新的 Stream
  • 操作是延迟执行的,只有触发终止操作时才会真正执行
  • 流只能被消费一次,遍历完就用完了

1.2 操作分类

1
2
3
4
5
6
7
Stream 操作
├── 中间操作(Intermediate)—— 返回新 Stream,延迟执行
│ ├── 无状态:filter、map、flatMap、peek
│ └── 有状态:distinct、sorted、limit、skip
└── 终止操作(Terminal)—— 触发计算,返回最终结果
├── 非短路:forEach、collect、reduce、count
└── 短路:anyMatch、allMatch、findFirst、findAny

二、创建 Stream 的四种方式

2.1 通过集合创建(最常用)

1
2
3
4
5
6
7
List<String> list = Arrays.asList("a", "b", "c");

// 顺序流
Stream<String> stream = list.stream();

// 并行流
Stream<String> parallelStream = list.parallelStream();

2.2 通过数组创建

1
2
3
4
5
String[] array = {"apple", "banana", "cherry"};
Stream<String> stream = Arrays.stream(array);

// 指定范围
Stream<String> partStream = Arrays.stream(array, 0, 2);

2.3 通过 Stream.of

1
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

2.4 创建无限流

1
2
3
4
5
// 迭代:从0开始,每次+2
Stream<Integer> iterate = Stream.iterate(0, x -> x + 2);

// 生成:随机数
Stream<Double> generate = Stream.generate(Math::random);

三、中间操作

3.1 filter — 过滤

筛选符合条件的元素:

1
2
3
4
5
6
7
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 — 映射转换

将元素转换为另一种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 — 扁平化映射

将流中每个元素都换成另一个流,然后把所有流合成一个流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 — 去重

1
2
3
4
5
6
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 — 排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 — 截断与跳过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 类似,但属于中间操作,常用于调试:

1
2
3
4
5
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 — 遍历

1
list.stream().forEach(System.out::println);

4.2 count — 计数

1
long count = list.stream().count();

4.3 max / min — 最值

1
2
3
4
5
6
// 最大值
Optional<Integer> max = list.stream().max(Integer::compareTo);

// 员工中年龄最大的
Optional<Employee> oldest = employees.stream()
.max(Comparator.comparing(Employee::getAge));

4.4 findFirst / findAny — 查找

1
2
3
4
5
// 找第一个
Optional<Integer> first = list.stream().findFirst();

// 找任意一个(并行流中效率更高)
Optional<Integer> any = list.parallelStream().findAny();

4.5 匹配操作

1
2
3
4
5
6
7
8
// 是否全部满足
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 — 归约

将流中元素反复结合,得到一个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 转集合

1
2
3
4
5
6
7
8
// 转 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

1
2
3
4
5
6
7
8
9
10
11
// 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

按某个字段分组,是开发中最常用的操作之一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 按部门分组
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 两组:

1
2
3
4
5
6
7
// 按薪资是否大于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 聚合统计

1
2
3
4
5
6
7
8
9
// 统计数量、总和、最小值、平均值、最大值
IntSummaryStatistics stats = employees.stream()
.collect(Collectors.summarizingInt(Employee::getAge));

stats.getCount(); // 总数
stats.getSum(); // 总和
stats.getMin(); // 最小值
stats.getAverage(); // 平均值
stats.getMax(); // 最大值

5.6 拼接字符串

1
2
3
4
5
6
7
8
9
// 用逗号拼接所有名字
String names = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));

// 带前后缀
String result = words.stream()
.collect(Collectors.joining(", ", "[", "]"));
// 结果:[Hello, Stream, World]

六、实战案例

案例一:从员工列表中筛选统计

1
2
3
4
5
6
7
8
// 需求:找出研发部门薪资大于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());

案例二:计算每个部门的薪资总和

1
2
3
4
5
Map<String, Double> deptTotalSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.summingDouble(Employee::getSalary)
));

案例三:嵌套集合扁平化

1
2
3
4
5
6
// 一个班级有多个学生,每个学生有多个爱好。找出所有爱好去重
List<String> allHobbies = classes.stream()
.flatMap(c -> c.getStudents().stream())
.flatMap(s -> s.getHobbies().stream())
.distinct()
.collect(Collectors.toList());

案例四:List 转树形结构

1
2
3
4
5
6
7
8
9
10
11
12
13
// 把扁平的菜单列表转成树形结构
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 使用方式

1
2
3
4
5
// 直接获取并行流
list.parallelStream()

// 普通流转并行
list.stream().parallel()

7.2 适用场景

  • 数据量大、计算密集型任务
  • 每个元素计算独立,无状态依赖
  • CPU 多核能发挥优势

7.3 注意事项

1
2
3
4
5
6
// ❌ 错误:并行流操作共享变量,结果不确定
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,配合使用能优雅避免空指针:

1
2
3
4
5
6
// 找到第一个薪资大于10000的员工,获取名字
String name = employees.stream()
.filter(emp -> emp.getSalary() > 10000)
.findFirst()
.map(Employee::getName)
.orElse("未找到");

Stream 是 Java 8 最实用的特性之一,熟练使用能大幅提升代码质量。但也要注意:不要为了用 Stream 而强行写复杂的一行流,可读性永远是第一位的。复杂逻辑拆成几步,比一行难以理解的长链更利于维护。