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 使用 IntStreamLongStream 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 资源。
  • 中间操作:零个或多个,如 filtermap
  • 终端操作:一个,如 collectcount

中间操作是懒惰的,只有在终端操作触发时才会执行。这种延迟执行(lazy evaluation)可以避免不必要的计算,提高性能。

示例:

1
2
3
4
5
List<String> list = Arrays.asList("abc1", "abc2", "abc3");
long count = list.stream()
.filter(s -> s.contains("2")) // 中间操作:过滤
.map(String::toUpperCase) // 中间操作:映射
.count(); // 终端操作:计数

在这个流水线中,只有当 count() 被调用时,filtermap 才会执行。

并行 Stream

Stream 支持并行处理,可以利用多核处理器。通过 parallelStream()parallel() 方法可以创建并行流。

示例:

1
2
3
List<String> list = Arrays.asList("a", "b", "c");
boolean allStartWithA = list.parallelStream()
.allMatch(s -> s.startsWith("a"));

需要注意的是,并行流并非总是更快。它们会引入分区和合并的开销,适合计算密集型任务(如复杂的映射或归约)。对于简单操作或小数据集,顺序流可能更高效。建议通过性能测试确定是否使用并行流。

实际应用场景

Stream 在实际开发中用途广泛,以下是一些典型场景:

集合处理

过滤价格高于 100 美元的产品并计算总价:

1
2
3
4
5
List<Product> products = ...;
double totalPrice = products.stream()
.filter(p -> p.getPrice() >= 100)
.mapToDouble(Product::getPrice)
.sum();

处理大型数据集

统计大文件的行数:

1
2
3
4
5
6
try (Stream<String> lines = Files.lines(Paths.get("largefile.txt"))) {
long lineCount = lines.count();
System.out.println("Number of lines: " + lineCount);
} catch (IOException e) {
e.printStackTrace();
}

并发处理

检查数字列表中是否存在大于 1000 的数字:

1
2
3
List<Integer> numbers = ...;
boolean hasLargeNumber = numbers.parallelStream()
.anyMatch(n -> n > 1000);

最佳实践和注意事项

  • 避免状态化操作:在并行流中,状态化操作(如修改外部变量)可能导致不可预期的结果。
  • 使用短路操作:如 findFirst()anyMatch(),可以在满足条件时提前终止流水线。
  • 内存管理:收集大型数据集可能导致内存溢出,考虑分块处理或外部存储。
  • 并行流开销:仅在计算密集型任务中使用并行流,并通过性能测试验证效果。
  • 熟悉前置知识:Stream 依赖 Java 8 的 Lambda 表达式、Optional 和方法引用,建议先掌握这些概念。

总结

Java Stream 提供了一种强大的数据处理方式,通过函数式编程的概念,使代码更简洁、可读。它支持延迟执行和并行处理,适合处理大型数据集或利用多核处理器。然而,Stream 并非万能,开发者需要根据场景选择合适的操作方式,并注意性能和内存问题。