一篇文章吃透Stream流
Java Stream
引入
在 Java 中,经常需要处理数组、Collection 等集合的操作,传统上我们会使用循环的方式来实现。然而,Java 8 引入了一种更加优雅和高效的方式——Stream API。Stream 允许我们以函数式编程的风格来处理数据序列(如集合、数组或 I/O 资源),它提供了声明式的数据处理方式,使代码更易读、更简洁,同时还能更好地利用多核处理器。
Stream 并不是一个数据结构,而是对数据进行计算的一种方式。它支持延迟执行(lazy evaluation),即只有在需要时才执行操作,这可以显著提高性能,尤其是在处理大型数据集时。此外,Stream 还支持并行处理,无需显式编写多线程代码即可利用多核 CPU。
接下来,我们将深入探索 Java Stream 的各个方面,包括它的创建、操作类型、流水线、并行处理以及实际应用场景。
创建 Stream
Stream 的创建方式多种多样,下面列出了一些常见的方法,并附上示例代码:
创建方式 | 描述 | 示例代码 |
---|---|---|
从集合 | 使用集合的 stream() 方法 |
List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream = list.stream(); |
从数组 | 使用 Arrays.stream(array) |
int[] numbers = {1, 2, 3}; IntStream stream = Arrays.stream(numbers); |
从 I/O 资源 | 使用 Files.lines(Path path) 读取文件行 |
Stream<String> lines = Files.lines(Paths.get("file.txt")); |
使用 Stream.of() | 创建包含指定元素的 Stream | Stream<String> stream = Stream.of("a", "b", "c"); |
空 Stream | 使用 Stream.empty() 创建空 Stream |
Stream<String> emptyStream = Stream.empty(); |
使用 Builder | 使用 Stream.builder() 构建 Stream |
Stream<String> stream = Stream.<String>builder().add("a").add("b").add("c").build(); |
生成和迭代 | 使用 Stream.generate() 或 Stream.iterate() |
Stream<Integer> evenNumbers = Stream.iterate(0, n -> n + 2); |
原始类型 Stream | 使用 IntStream 、LongStream 等 |
IntStream.range(1, 10); |
从字符串 | 使用字符串的 chars() 方法 |
"hello".chars(); |
这些方法覆盖了大多数常见场景,开发者可以根据数据源选择合适的方式。
Stream 操作
Stream 支持两种类型的操作:中间操作(Intermediate Operations)和终端操作(Terminal Operations)。
中间操作
中间操作会返回一个新的 Stream,可以被链式调用。它们是懒惰执行的,只有当终端操作被调用时才会执行。以下是常见的中间操作:
操作 | 描述 | 示例 |
---|---|---|
filter(Predicate) |
根据条件过滤元素 | stream.filter(s -> s.startsWith("a")) |
map(Function) |
将元素映射为新值 | stream.map(String::toUpperCase) |
sorted() |
对元素排序 | stream.sorted() |
distinct() |
去除重复元素 | stream.distinct() |
peek(Consumer) |
对元素执行操作(不修改流) | stream.peek(System.out::println) |
limit(long) |
限制元素数量 | stream.limit(5) |
skip(long) |
跳过前指定数量的元素 | stream.skip(2) |
终端操作
终端操作会产生一个结果或副作用,并结束流水线。以下是常见的终端操作:
操作 | 描述 | 示例 |
---|---|---|
forEach(Consumer) |
对每个元素执行操作 | stream.forEach(System.out::println) |
collect(Collector) |
收集元素到集合 | stream.collect(Collectors.toList()) |
reduce(BinaryOperator) |
归约为单一值 | stream.reduce(0, Integer::sum) |
count() |
返回元素数量 | stream.count() |
min(Comparator) |
返回最小元素 | stream.min(Comparator.naturalOrder()) |
max(Comparator) |
返回最大元素 | stream.max(Comparator.naturalOrder()) |
allMatch(Predicate) |
检查是否所有元素满足条件 | stream.allMatch(s -> s.length() > 0) |
anyMatch(Predicate) |
检查是否有元素满足条件 | stream.anyMatch(s -> s.startsWith("a")) |
noneMatch(Predicate) |
检查是否没有元素满足条件 | stream.noneMatch(s -> s.isEmpty()) |
findFirst() |
返回第一个元素 | stream.findFirst() |
findAny() |
返回任意元素 | stream.findAny() |
Stream 流水线
Stream 的核心特性是可以通过链式调用形成流水线。一个完整的流水线包括:
- 源:如集合、数组或 I/O 资源。
- 中间操作:零个或多个,如
filter
、map
。 - 终端操作:一个,如
collect
、count
。
中间操作是懒惰的,只有在终端操作触发时才会执行。这种延迟执行(lazy evaluation)可以避免不必要的计算,提高性能。
示例:
1 | List<String> list = Arrays.asList("abc1", "abc2", "abc3"); |
在这个流水线中,只有当 count()
被调用时,filter
和 map
才会执行。
并行 Stream
Stream 支持并行处理,可以利用多核处理器。通过 parallelStream()
或 parallel()
方法可以创建并行流。
示例:
1 | List<String> list = Arrays.asList("a", "b", "c"); |
需要注意的是,并行流并非总是更快。它们会引入分区和合并的开销,适合计算密集型任务(如复杂的映射或归约)。对于简单操作或小数据集,顺序流可能更高效。建议通过性能测试确定是否使用并行流。
实际应用场景
Stream 在实际开发中用途广泛,以下是一些典型场景:
集合处理
过滤价格高于 100 美元的产品并计算总价:
1 | List<Product> products = ...; |
处理大型数据集
统计大文件的行数:
1 | try (Stream<String> lines = Files.lines(Paths.get("largefile.txt"))) { |
并发处理
检查数字列表中是否存在大于 1000 的数字:
1 | List<Integer> numbers = ...; |
最佳实践和注意事项
- 避免状态化操作:在并行流中,状态化操作(如修改外部变量)可能导致不可预期的结果。
- 使用短路操作:如
findFirst()
、anyMatch()
,可以在满足条件时提前终止流水线。 - 内存管理:收集大型数据集可能导致内存溢出,考虑分块处理或外部存储。
- 并行流开销:仅在计算密集型任务中使用并行流,并通过性能测试验证效果。
- 熟悉前置知识:Stream 依赖 Java 8 的 Lambda 表达式、Optional 和方法引用,建议先掌握这些概念。
总结
Java Stream 提供了一种强大的数据处理方式,通过函数式编程的概念,使代码更简洁、可读。它支持延迟执行和并行处理,适合处理大型数据集或利用多核处理器。然而,Stream 并非万能,开发者需要根据场景选择合适的操作方式,并注意性能和内存问题。