【后端】Java Stream API 介绍
文章目录
- 核心概念
- 主要特点
- 使用 Stream 的步骤
- 一个简单示例
- 重要注意事项
- 为什么使用 Stream API?
- 总结
核心概念
- 核心思想: Stream API 提供了一种函数式、声明式的方法来处理数据序列(流)。你只需描述要做什么(例如,过滤、转换、排序、聚合),而无需指定具体如何一步步做(如手动写
for
循环)。 - 什么是 Stream:
- 不是数据结构:Stream 本身并不存储数据。它代表一个来自数据源(集合、数组、I/O 通道、生成器函数等)的元素序列,让你在这些元素上实施各种计算操作。
- 单向流水线:你可以将 Stream 想象成一个数据处理流水线,数据从源头开始,依次流经一个或多个中间操作(进行转换、筛选等),最终通过一个终端操作产生结果(或副作用)。
- 不可复用:一个 Stream 一旦被消费(执行了终端操作),就不能再被使用。
主要特点
- 声明式编程: 代码更接近问题陈述,更易读、更简洁。例如,“获取列表中大于 10 的偶数,并将其平方”可以直接表达为一连串操作。
- 可组合性 (Composable): 中间操作(如
filter
,map
,sorted
)可以像搭积木一样链接起来,形成复杂的数据处理流水线。 - 内部迭代: Stream 在内部处理迭代细节(遍历元素),你不再需要编写显式的
for
或iterator
循环。这减少了模板代码。 - 惰性求值 (Lazy Execution): 中间操作通常都是惰性的。定义中间操作(如
filter(predicate)
)本身不会立即执行任何实际操作,它们只是记录在流水线上。只有执行终端操作(如collect
,forEach
,count
)时,整个流水线才会被触发执行,并且通常会进行尽可能多的优化(比如短路操作,findFirst
找到符合条件的第一个元素后就不再遍历后面元素)。 - 可能的并行化: 通过简单地调用
parallelStream()
或stream().parallel()
,就能将数据处理任务(如果操作是无状态的)透明地并行执行在多核 CPU 上,极大提升大数据集的处理效率。Stream API 会自动处理底层的线程管理。
使用 Stream 的步骤
通常遵循以下模式:
- 获取 Stream:
- 从集合:
collection.stream()
(顺序流) 或collection.parallelStream()
(并行流)。 - 从数组:
Arrays.stream(array)
。 - 从值:
Stream.of(val1, val2, ..., valN)
。 - 无限流:
Stream.iterate(initialSeed, UnaryOperator)
或Stream.generate(Supplier)
。 - 其他:I/O 通道 (
Files.lines(path)
),随机数生成器等。
- 从集合:
- 应用中间操作 (0-N 个): 将一个 Stream 转化为另一个 Stream。
filter(Predicate<T>)
: 过滤元素,只保留满足条件的。map(Function<T, R>)
: 将每个元素转换成另一种形式(T -> R)。flatMap(Function<T, Stream<R>>)
: 将每个元素映射成一个 Stream,然后将所有流“拍平”连接成一个 Stream。distinct()
: 去重(依赖equals
)。sorted()
或sorted(Comparator<T>)
: 排序。limit(long n)
: 截断流,取前 n 个元素。skip(long n)
: 跳过前 n 个元素。peek(Consumer<T>)
: 对每个元素执行操作(调试或记录常用),但不改变流本身。
- 执行终端操作 (1 个): 产生最终结果或副作用。执行后,Stream 就被“消费”掉,不能再使用。
- 聚合/计算:
count()
: 返回流中元素个数。min(Comparator<T>)
,max(Comparator<T>)
: 查找最小/最大值(返回Optional<T>
)。reduce(...)
: 通过累积操作(如累加、拼接)将流缩减为一个值(例如计算总和reduce(0, (a, b) -> a + b)
)。有多种重载。allMatch(Predicate<T>)
/anyMatch(Predicate<T>)
/noneMatch(Predicate<T>)
: 检查元素是否全部、任意或没有匹配谓词。findFirst()
/findAny()
: 返回流中第一个或任意一个元素(返回Optional<T>
)。
- 收集结果:
collect(Collector<T, A, R>)
: 最灵活强大的终端操作。使用预定义或自定义的收集器(如Collectors.toList()
,Collectors.toSet()
,Collectors.toMap(...)
,Collectors.groupingBy(...)
,Collectors.joining()
等)将流转换为集合、Map、字符串或其他复杂结构。
- 遍历/副作用:
forEach(Consumer<T>)
: 对每个元素执行操作(通常在生成最终结果后使用)。
- 转换:
toArray()
/toArray(IntFunction<A[]>)
: 将流转换为数组。
- 聚合/计算:
一个简单示例
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);// 传统方式:获取偶数,平方它们,然后存到新列表
List<Integer> evenSquaresOld = new ArrayList<>();
for (Integer number : numbers) {if (number % 2 == 0) {evenSquaresOld.add(number * number);}
}// 使用 Stream API:清晰描述意图
List<Integer> evenSquares = numbers.stream() // 1. 获取顺序流.filter(n -> n % 2 == 0) // 2. 过滤出偶数 (中间操作).map(n -> n * n) // 3. 平方每个元素 (中间操作).collect(Collectors.toList()); // 4. 收集结果到List (终端操作)System.out.println(evenSquares); // 输出: [4, 16, 36, 64, 100]
可以看到,Stream API 的代码更简洁、更易读(声明式),清晰表达了“过滤 -> 转换 -> 收集”的处理逻辑。
重要注意事项
- 不可复用: 每个终端操作都会消费掉流。尝试再次使用已消费的流会抛出
IllegalStateException
。 - 无副作用(原则): 中间操作(尤其是 lambda 表达式)理想情况下应该是无状态的(Stateless)并且不产生副作用(不要修改外部状态)。这有利于并行执行和正确性。如果必须访问外部状态(如累加器),需要特别小心线程安全问题(尤其是并行流)。使用
forEach
或peek
进行副作用操作时也要谨慎。 - 并行流: 虽然
parallelStream()
可以带来性能提升,但并非总是更快。它有额外的线程管理开销。在以下情况下使用可能得不偿失:- 数据量很小。
- 操作本身计算量很轻。
- 操作涉及共享可变状态(需要额外同步,可能抵消并行收益)。
- 底层数据源不易分割(如
LinkedList
)。 - 使用得当是关键,应先测试性能。
Optional<T>
: 像findFirst()
,max()
这样的终端操作返回Optional<T>
。这是一种避免NullPointerException
的容器对象,表示结果可能为空。你需要正确处理(isPresent()
,get()
,orElse(...)
,orElseGet(...)
,orElseThrow(...)
)。- 与集合的区别: Stream 不是集合,不存储数据。它是数据处理工具。你可以从集合获取流来处理数据,处理结果也可以通过
collect
存回集合。
为什么使用 Stream API?
- 简洁性: 显著减少处理数据的模板代码。
- 可读性: 代码更能表达业务逻辑意图(“做什么”而非“如何做”)。
- 并行潜力: 轻松利用多核性能(几乎无需修改代码)。
- 灵活性: 强大的中间操作组合能处理各种复杂的数据转换和筛选需求。
总结
Java Stream API 是现代 Java 编程的核心部分。掌握它能够让你用更高效、更优雅、更不易出错的方式来处理数据和集合。它代表了 Java 向函数式编程风格的转变,极大地提升了数据处理代码的生产力和表现力。从简单的过滤转换到复杂的聚合分组,Stream API 都提供了强大的工具。建议在合适的场景中积极使用,以取代传统的 for
循环和迭代器操作。