1. 概述
Java 8 的 Stream API 提供了一种比 Java 集合框架 更高效的处理结果集的方式。但在实际开发中,我们经常需要在两者之间做出选择。
本文将深入探讨 Stream 和 Collection 各自的适用场景,并结合具体示例帮助你做出更合理的技术决策。
2. Collection vs. Stream
Java 中的 Collection 提供了存储和操作数据的机制,包括常见的数据结构如 List、Set 和 Map。
而 Stream API 则专注于对数据进行处理操作,无需中间存储结构,类似于直接从底层资源(如集合或 I/O)读取数据。
✅ 关键区别:
- Collection:强调数据的存储和访问,支持修改操作。
- Stream:强调数据的传输与处理,不支持修改源数据。
虽然 Java 提供了便捷的 API 在 Collection 和 Stream 之间互相转换,但了解何时使用哪一个才是关键。
例如,可以使用 stream()
或 parallelStream()
方法将 Collection 转换为 Stream:
public Stream<String> userNames() {
ArrayList<String> userNameSource = new ArrayList<>();
userNameSource.add("john");
userNameSource.add("smith");
userNameSource.add("tom");
return userNameSource.stream();
}
同样地,可以通过 collect()
方法将 Stream 转换回 Collection:
public List<String> userNameList() {
return userNames().collect(Collectors.toList());
}
也可以转换为 Set 或 Map:
public static Set<String> userNameSet() {
return userNames().collect(Collectors.toSet());
}
public static Map<String, String> userNameMap() {
return userNames().collect(Collectors.toMap(u1 -> u1.toString(), u1 -> u1.toString()));
}
3. 何时返回 Stream?
3.1. 高物化成本 ❌
如果物化整个结果集代价很高,比如需要读取大文件或处理大量数据,返回 Stream 是更优选择。
因为 Stream 是惰性求值的,可以按需处理数据,避免不必要的内存开销。
例如,使用 Files.lines()
返回一个 Stream,并配合 limit()
限制结果数量:
Files.lines(path).limit(10).collect(toList());
中间操作(如 filter
)不会立即执行,直到遇到终端操作(如 forEach
):
userNames().filter(i -> i.length() >= 4).forEach(System.out::println);
✅ 优点:避免提前物化带来的性能损耗。
3.2. 大量或无限结果 ✅
Stream 天生适合处理大量数据甚至无限数据流。
例如,生成无限序列并只取前几个元素:
Stream.iterate(0, n -> n + 1)
.limit(10)
.forEach(System.out::println);
⚠️ 注意:Collection 无法处理无限数据,因为必须一次性加载所有数据。
3.3. 灵活性高 ✅
Stream 提供了丰富的链式操作,如 filter
、sorted
、limit
等,消费者可以根据需求灵活处理数据。
示例:
public static Stream<String> filterUserNames() {
return userNames().filter(i -> i.length() >= 4);
}
public static Stream<String> sortUserNames() {
return userNames().sorted();
}
public static Stream<String> limitUserNames() {
return userNames().limit(3);
}
3.4. 函数式行为 ✅
Stream 是不可变的,所有的中间操作都会返回新的 Stream,不会修改原始数据源。
例如:
userNames().filter(i -> i.length() >= 4).limit(3).forEach(System.out::println);
这里的 filter
和 limit
都不会改变原始 Stream,非常适合需要保持数据不可变的场景。
4. 何时返回 Collection?
4.1. 物化成本低 ✅
当结果集较小,物化代价不高时,使用 Collection 更直观,也更容易调试和使用。
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
⚠️ 注意:大结果集会占用大量内存,慎用。
4.2. 固定格式 ✅
如果需要返回一个固定格式或顺序的结果集,使用 Collection 更合适。
例如,使用 TreeSet 可以保证自然排序:
Set<String> sortedNames = new TreeSet<>(names);
4.3. 结果可重复遍历 ✅
Stream 是一次性的,遍历后即被消费,再次使用会抛出异常:
public static void tryStreamTraversal() {
Stream<String> userNameStream = userNames();
userNameStream.forEach(System.out::println);
try {
userNameStream.forEach(System.out::println); // ❌ 抛出异常
} catch(IllegalStateException e) {
System.out.println("stream has already been operated upon or closed");
}
}
而 Collection 可以多次遍历,适合需要重复访问的场景。
4.4. 允许修改 ✅
Collection 支持对数据的增删改操作,适合需要返回可变结果的场景:
List<String> names = userNameList();
names.add("bob");
names.remove(2);
Map 也支持 put
和 remove
:
Map<String, String> userNameMap = userNameMap();
userNameMap.put("bob", "bob");
userNameMap.remove("alfred");
4.5. 已在内存中的结果 ✅
如果结果已经存在于内存中(如已有 List),直接返回 Collection 更自然。
5. 总结
场景 | 使用 Stream | 使用 Collection |
---|---|---|
大量或无限数据 | ✅ | ❌ |
需要惰性计算 | ✅ | ❌ |
需要多次遍历 | ❌ | ✅ |
需要修改数据 | ❌ | ✅ |
结果已存在内存 | ❌ | ✅ |
需要排序或固定格式 | ❌ | ✅ |
简单来说:
- Stream:适合处理大/无限数据、强调不可变性、支持链式操作。
- Collection:适合小数据、需要修改、固定格式、多次遍历等场景。
👉 完整代码示例可在 GitHub 获取。