Java Stream API 实战指南:3个核心概念让你告别循环嵌套
前言:你还在写这种代码吗?
java
List<String> result = new ArrayList<>();
for (String s : list) {
if (s.length() > 3) {
result.add(s.toUpperCase());
}
}
如果是,那这篇文章就是为你准备的。Java 8 的 Stream API 让你可以用一行代码搞定上述逻辑,而且更优雅、更高效、更易维护。
读完本文,你将掌握:
- 4种 Stream 创建方式,应对不同数据源
- 中间操作与终结操作的本质区别(避免掉坑)
- 3个最常用 Collectors,从列表到映射全搞定
一、Stream 创建:流水线的起点
1. 从集合创建(最常用)
java
List<String> list = Arrays.asList("Java", "Stream", "Collection");
// 顺序流:保持元素原有顺序
Stream<String> stream = list.stream();
// 并行流:大数据量时自动多线程处理
Stream<String> parallelStream = list.parallelStream();
什么时候用并行流? 数据量超过 1 万元素,且操作无状态(如 filter、map)时考虑使用。
2. 直接创建元素流
java
// 快速创建流
Stream<String> fruits = Stream.of("Apple", "Banana", "Cherry");
// 创建空流(避免 NPE 的最佳实践)
Stream<String> empty = Stream.empty();
场景技巧:处理可能为 null 的集合时,用 Optional.ofNullable(list).orElseGet(Collections::emptyList).stream() 替代空判断。
3. 从数组创建
java
// 引用类型数组
String[] colors = {"Red", "Green", "Blue"};
Stream<String> colorStream = Arrays.stream(colors);
// 基本类型数组(避免自动装箱)
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
// 截取数组部分(左闭右开)
IntStream part = Arrays.stream(numbers, 1, 4); // [2, 3, 4]
性能提示:处理 int[]、long[]、double[] 时优先用 IntStream/LongStream/DoubleStream,避免大量装箱拆箱。
4. 从文件按行读取
java
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
lines.filter(line -> !line.isBlank())
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
适用场景:大文件处理(GB 级别),流式读取不会把整个文件加载到内存。
二、中间操作 vs 终结操作:理解惰性求值
核心对比
| 特性 | 中间操作 | 终结操作 |
|---|---|---|
| 返回值 | 新的 Stream(支持链式调用) | 具体结果(List、Integer、void 等) |
| 执行时机 | 惰性执行(仅记录规则) | 触发实际处理 |
| 流状态 | 可继续操作 | 流被消费,不可再用 |
常用方法速查
中间操作: filter()、map()、sorted()、distinct()、limit()、skip()
终结操作: forEach()、collect()、count()、reduce()、anyMatch()、findFirst()
惰性求值的威力
看这个例子:
java
Stream.of(1, 2, 3, 4, 5, 6)
.filter(n -> {
System.out.println("filter: " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("map: " + n);
return n * n;
})
.collect(Collectors.toList());
输出:
plaintext
filter: 1
filter: 2
map: 2
filter: 3
filter: 4
map: 4
filter: 5
filter: 6
map: 6
关键洞察:
- 只有遇到
collect()时,整个流水线才开始执行 - 每个元素流经所有中间操作,一次性处理完成
- 如果加上
.limit(2),会在找到 2 个结果后立即停止,不会遍历全部数据
性能技巧:将 filter() 放在 map() 之前,尽早减少数据量。
三、Collectors 实战:3个常用收集器
1. toList():收集到列表
java
// 基础用法
List<String> fruits = Stream.of("Apple", "Banana", "Cherry")
.collect(Collectors.toList());
// 结合筛选
List<Integer> evens = Stream.of(1, 2, 3, 4, 5)
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
JDK 16+ 可直接用 .toList(),返回的是不可变列表。
2. toMap():收集到映射
java
// 基础:对象流 → Map
Map<String, String> userMap = users.stream()
.collect(Collectors.toMap(
User::getId, // key
User::getName // value
));
// 处理重复键(保留旧值)
Map<String, String> safeMap = users.stream()
.collect(Collectors.toMap(
User::getId,
User::getName,
(oldVal, newVal) -> oldVal // 合并策略
));
// 简单类型:数字 → 平方
Map<Integer, Integer> squares = Stream.of(1, 2, 3)
.collect(Collectors.toMap(
n -> n,
n -> n * n
));
// 结果:{1=1, 2=4, 3=9}
注意:不指定合并策略时,遇到重复键会抛出 IllegalStateException。
3. joining():字符串拼接
java
// 直接拼接(无分隔符)
String s1 = Stream.of("Java", "Stream").collect(Collectors.joining());
// "JavaStream"
// 指定分隔符
String s2 = Stream.of("Java", "Stream").collect(Collectors.joining("-"));
// "Java-Stream"
// 完整格式:分隔符 + 前缀 + 后缀
String s3 = Stream.of("Java", "Stream", "API")
.collect(Collectors.joining(", ", "[", "]"));
// "[Java, Stream, API]"
// 非字符串流:先转换
String nums = Stream.of(1, 2, 3)
.map(String::valueOf)
.collect(Collectors.joining("|"));
// "1|2|3"
性能提示:joining() 底层使用 StringBuilder,比手动 + 拼接快很多,适合拼接大量字符串。
四、实战速查表
| 需求 | 代码片段 |
|---|---|
| 过滤奇数 | .filter(n -> n % 2 == 0) |
| 转大写 | .map(String::toUpperCase) |
| 去重 | .distinct() |
| 排序 | .sorted() / .sorted(Comparator.reverseOrder()) |
| 取前3个 | .limit(3) |
| 跳过前2个 | .skip(2) |
| 转数组 | .toArray(String[]::new) |
| 计数 | .count() |
| 求和 | .mapToInt(Integer::intValue).sum() |
| 任意匹配 | .anyMatch(n -> n > 10) |
结语:这才是现代 Java 的样子
Stream API 不是炫技工具,而是让代码更接近业务意图的方式。当你习惯用声明式风格思考数据流,会发现代码变得:
- 更简洁:一行替代多行循环嵌套
- 更安全:减少索引错误、空指针风险
- 更高效:惰性求值 + 并行流优化性能
评论区交流: 你在项目中用 Stream API 遇到过哪些坑?或者有什么独门技巧?欢迎分享!