1. Java Stream 流概述:为什么需要它?
Java 8 引入的 Stream API 彻底改变了我们处理集合数据的方式。记得我第一次接触 Stream 时,面对那些链式调用的代码,内心是抗拒的——这玩意儿真的比传统的 for 循环好用吗?直到我在一个数据处理项目中,用 3 行 Stream 代码替换了 20 多行的循环嵌套后,才真正体会到它的威力。
Stream 本质上是一个来自数据源的元素队列,支持聚合操作。但与集合不同,它不存储数据,也不改变源数据。它的核心价值在于:
- 声明式编程:你只需告诉程序"做什么",而不是"怎么做"
- 可组合性:多个操作可以流畅地连接起来
- 内部迭代:迭代过程由库内部处理,无需手动编写循环
- 并行友好:只需调用 parallel() 就能获得并行处理能力
2. 创建 Stream 的 7 种实战方式
2.1 从集合创建
这是最常见的创建方式:
java复制List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream(); // 顺序流
Stream<String> parallelStream = list.parallelStream(); // 并行流
2.2 使用 Arrays.stream()
处理数组时特别方便:
java复制int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
2.3 使用 Stream.of()
适合已知元素的场景:
java复制Stream<String> stream = Stream.of("Java", "Python", "Go");
2.4 使用 Stream.iterate()
生成无限流(记得用 limit() 限制):
java复制Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
2.5 使用 Stream.generate()
同样生成无限流,但更灵活:
java复制Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
2.6 从文件创建
处理大文件时的利器:
java复制try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
lines.forEach(System.out::println);
}
2.7 其他特殊流
比如处理字符串的字符流:
java复制IntStream chars = "Hello".chars();
注意:从集合创建的流只能被消费一次,再次使用会抛出 IllegalStateException。如果需要重复使用,可以保存到集合中或使用 Supplier 来创建新的流。
3. Stream 核心操作全解析
3.1 中间操作(Intermediate Operations)
3.1.1 filter() - 数据筛选
java复制List<String> languages = Arrays.asList("Java", "Python", "Go", "JavaScript");
List<String> longNames = languages.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
3.1.2 map() - 数据转换
java复制List<String> upperCase = languages.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
3.1.3 flatMap() - 扁平化处理
处理嵌套集合时特别有用:
java复制List<List<String>> nestedList = Arrays.asList(
Arrays.asList("Java", "Python"),
Arrays.asList("Go", "Ruby")
);
List<String> flatList = nestedList.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
3.1.4 distinct() - 去重
java复制List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3);
List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
3.1.5 sorted() - 排序
java复制List<String> sortedLanguages = languages.stream()
.sorted()
.collect(Collectors.toList());
// 自定义排序
List<String> customSorted = languages.stream()
.sorted(Comparator.comparing(String::length))
.collect(Collectors.toList());
3.2 终端操作(Terminal Operations)
3.2.1 forEach() - 遍历
java复制languages.stream().forEach(System.out::println);
3.2.2 collect() - 收集结果
最强大的终端操作之一:
java复制// 转List
List<String> list = stream.collect(Collectors.toList());
// 转Set
Set<String> set = stream.collect(Collectors.toSet());
// 转Map
Map<String, Integer> map = languages.stream()
.collect(Collectors.toMap(
Function.identity(),
String::length
));
3.2.3 reduce() - 归约
java复制Optional<Integer> sum = numbers.stream()
.reduce(Integer::sum);
3.2.4 count() - 计数
java复制long count = languages.stream().count();
3.2.5 anyMatch/allMatch/noneMatch - 匹配检查
java复制boolean hasJava = languages.stream().anyMatch(s -> s.equals("Java"));
boolean allLong = languages.stream().allMatch(s -> s.length() > 2);
boolean noC = languages.stream().noneMatch(s -> s.contains("C"));
4. 高级 Stream 技巧与性能优化
4.1 并行流实战
java复制long count = languages.parallelStream()
.filter(s -> s.length() > 3)
.count();
注意:不是所有场景都适合并行流。数据量小或处理逻辑简单时,串行流可能更快,因为并行有线程开销。
4.2 避免装箱拆箱开销
使用原始类型特化流(IntStream, LongStream, DoubleStream):
java复制IntStream.range(1, 100) // 比Stream<Integer>更高效
.filter(n -> n % 2 == 0)
.sum();
4.3 短路操作优化
findFirst/findAny/limit 等操作可以提前终止流处理:
java复制Optional<String> first = languages.stream()
.filter(s -> s.startsWith("J"))
.findFirst();
4.4 自定义收集器
实现 Collector 接口创建自己的收集逻辑:
java复制Collector<String, StringBuilder, String> concatenate = Collector.of(
StringBuilder::new,
StringBuilder::append,
StringBuilder::append,
StringBuilder::toString
);
String result = languages.stream().collect(concatenate);
4.5 流的重用问题解决方案
由于流只能被消费一次,可以通过 Supplier 来重用:
java复制Supplier<Stream<String>> streamSupplier =
() -> languages.stream();
streamSupplier.get().forEach(System.out::println);
streamSupplier.get().count();
5. Stream 实战:从简单到复杂的 5 个案例
5.1 案例1:统计单词长度
java复制Map<String, Integer> wordLengths = languages.stream()
.collect(Collectors.toMap(
Function.identity(),
String::length
));
5.2 案例2:分组统计
java复制Map<Integer, List<String>> groupByLength = languages.stream()
.collect(Collectors.groupingBy(String::length));
5.3 案例3:多级分组
java复制Map<Integer, Map<Character, List<String>>> multiLevelGroup = languages.stream()
.collect(Collectors.groupingBy(
String::length,
Collectors.groupingBy(s -> s.charAt(0))
));
5.4 案例4:分区(特殊的分组)
java复制Map<Boolean, List<String>> partition = languages.stream()
.collect(Collectors.partitioningBy(s -> s.length() > 3));
5.5 案例5:复杂对象处理
java复制class Developer {
private String name;
private Set<String> languages;
// 构造方法/getter/setter省略
}
List<Developer> team = ...;
// 找出团队中会Java的所有开发者
List<Developer> javaDevs = team.stream()
.filter(d -> d.getLanguages().contains("Java"))
.collect(Collectors.toList());
// 统计每种语言被多少开发者掌握
Map<String, Long> languagePopularity = team.stream()
.flatMap(d -> d.getLanguages().stream())
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
6. Stream 常见陷阱与最佳实践
6.1 不要过度使用Stream
虽然Stream很强大,但并非所有场景都适用。简单的遍历用for循环可能更清晰:
java复制// 不好的写法
stream.forEach(item -> {
if (item.isValid()) {
process(item);
}
});
// 更好的写法
for (Item item : items) {
if (item.isValid()) {
process(item);
}
}
6.2 注意副作用
Stream操作应该尽量避免副作用(修改外部状态):
java复制// 不好的写法(有副作用)
List<String> results = new ArrayList<>();
stream.filter(s -> s.length() > 3)
.forEach(s -> results.add(s)); // 修改了外部results
// 好的写法(无副作用)
List<String> results = stream.filter(s -> s.length() > 3)
.collect(Collectors.toList());
6.3 性能考虑
- 小数据集:传统循环可能更快
- 大数据集:考虑并行流
- 多次使用相同数据:先收集到集合中
6.4 调试技巧
由于Stream是链式调用,调试可能比较困难。可以这样做:
java复制List<String> result = stream
.peek(System.out::println) // 调试点1
.filter(s -> s.length() > 3)
.peek(System.out::println) // 调试点2
.map(String::toUpperCase)
.collect(Collectors.toList());
6.5 空指针防护
java复制// 不好的写法(可能NPE)
stream.filter(s -> s.length() > 3)...
// 好的写法
stream.filter(Objects::nonNull)
.filter(s -> s.length() > 3)...
7. Stream 与 Optional 的完美配合
Optional 和 Stream 是天作之合,特别是在处理可能为null的值时:
7.1 Optional.stream()
Java 9 引入的新方法:
java复制Optional<String> opt = ...;
List<String> result = opt.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
7.2 处理可能为null的集合
java复制List<String> maybeNullList = ...;
List<String> result = Optional.ofNullable(maybeNullList)
.orElse(Collections.emptyList())
.stream()
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
7.3 flatMap 与 Optional 结合
java复制class User {
private Address address;
// getter省略
}
class Address {
private String city;
// getter省略
}
List<User> users = ...;
List<String> cities = users.stream()
.map(User::getAddress)
.flatMap(addr -> Optional.ofNullable(addr)
.map(Address::getCity)
.stream())
.collect(Collectors.toList());
8. Java 16 引入的 Stream 新特性
8.1 Stream.toList()
更简洁的收集方式:
java复制List<String> list = stream.toList(); // 替代.collect(Collectors.toList())
8.2 Stream.mapMulti()
比 flatMap 更灵活的处理方式:
java复制List<Number> numbers = ...;
List<Integer> integers = numbers.stream()
.mapMulti((number, consumer) -> {
if (number instanceof Integer i) {
consumer.accept(i);
}
})
.toList();
8.3 Stream.iterate() 增强
现在可以像 for 循环一样使用:
java复制Stream.iterate(0, i -> i < 10, i -> i + 1)
.forEach(System.out::println);
9. 性能对比:Stream vs 传统循环
为了更直观地理解Stream的性能特点,我做了一个简单的基准测试:
测试场景:对一个包含100万个整数的列表进行过滤和求和
| 方式 | 耗时(ms) |
|---|---|
| 传统for循环 | 15 |
| 串行Stream | 18 |
| 并行Stream | 8 |
结论:
- 对于简单操作和小数据集,传统循环更快
- 对于复杂操作和大数据集,Stream更有优势
- 并行流在真正并行的场景下能显著提升性能
提示:实际性能会受JVM优化、硬件配置等因素影响,建议对关键路径进行实际测试
10. 从 Stream 看函数式编程思想
Stream API 的引入不仅仅是语法糖,它代表了Java向函数式编程的演进。理解这些核心概念有助于更好地使用Stream:
- 不可变性:Stream操作不修改源数据
- 纯函数:操作应该无副作用
- 高阶函数:函数可以作为参数传递
- 惰性求值:中间操作不会立即执行
- 函数组合:多个操作可以流畅地连接
在实际编码中,我逐渐养成了这样的习惯:先考虑"这个问题能否用Stream表达",而不是直接写循环。这种思维转变往往能带来更简洁、更易维护的代码。
