Java8函数式编程之Stream API
今天,我们来全面、深入地介绍一下 Java 8 函数式编程中最具革命性的特性—— Stream API。
Stream API 是 Java 中处理集合(Collection
)数据的重大升级。它允许你以声明式(Declarative)、类似于 SQL 语句的风格来操作数据,此外,它还能非常方便地利用多核架构进行并行处理,而无需编写复杂的多线程代码。
1. 核心思想:它是什么?不是什么?
什么是 Stream?
- 它不是数据结构:它不存储数据,而是对数据源(通常是集合)进行计算操作的流水线。
- 它是高级迭代器:它提供了丰富的操作(过滤、映射、排序、归约等),让你可以透明地并行处理数据,而无需自己写
for
或while
循环。 - ** functional in nature**:它对源数据进行的操作不会修改源数据本身(它会产生一个新的 Stream),并且操作通常是无状态和无副作用的。
Stream vs. Collection
- Collection 主要关心的是数据的存储和高效访问(如
ArrayList
,HashSet
)。 - Stream 主要关心的是数据的计算和处理(如过滤、查找、转换、聚合)。
一个生动的比喻:
集合就像是一个存储了未加工原料的仓库(比如一箱生土豆)。
Stream 就像是一条土豆加工流水线。这条流水线会依次进行一系列操作:将土豆从箱中取出(获取流)-> 清洗土豆(过滤 filter
)-> 削皮(转换 map
)-> 切成薯条(转换 map
)-> 按尺寸分拣(排序 sorted
)-> 打包(终结操作 collect
)。流水线本身不存储土豆,它只是对土豆进行处理。
2. Stream 操作的三个步骤
使用 Stream 通常需要三个步骤,形成一个操作链(流水线):
- 创建 Stream:从一个数据源(如集合、数组、I/O通道)获取一个流。
- 中间操作:在一个或多个中间操作中,对 Stream 进行一系列处理(如过滤、映射),这些操作返回一个新的 Stream,因此可以链式调用。重要:中间操作是惰性的,在终结操作被调用前,它们不会真正执行。
- 终结操作:执行整个流水线并产生结果。执行后,该流就被消费掉了,不能再使用。结果可以是一个新集合、一个值、或者什么都不返回(如
forEach
)。
3. 创建 Stream
// 1. 从集合创建 (最常用)
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream1 = list.stream(); // 顺序流
Stream<String> parallelStream = list.parallelStream(); // 并行流// 2. 从数组创建
String[] array = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(array);// 3. 使用 Stream.of() 静态方法
Stream<String> stream3 = Stream.of("a", "b", "c");// 4. 创建无限流 (通常与 limit() 结合使用)
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2); // 无限偶数流
Stream<Double> randomStream = Stream.generate(Math::random); // 无限随机数流
4. 中间操作
中间操作是构建流水线的核心,它们会返回一个新的 Stream。
操作 | 方法签名 | 描述 | 示例 |
---|---|---|---|
过滤 | Stream<T> filter(Predicate<T>) | 排除不满足条件的元素 | stream.filter(s -> s.startsWith("A")) |
映射 | <R> Stream<R> map(Function<T, R>) | 将元素转换为其他形式或提取信息 | stream.map(String::toUpperCase) stream.map(s -> s.length()) |
去重 | Stream<T> distinct() | 通过 hashCode() 和 equals() 去重 | stream.distinct() |
排序 | Stream<T> sorted() Stream<T> sorted(Comparator<T>) | 产生一个按自然顺序或比较器排序的新流 | stream.sorted() stream.sorted(Comparator.reverseOrder()) |
截取 | Stream<T> limit(long maxSize) | 使其元素不超过给定数量 | stream.limit(10) |
跳过 | Stream<T> skip(long n) | 跳过前N个元素 | stream.skip(5) |
** peek ** | Stream<T> peek(Consumer<T>) | 主要用于调试,查看流经流的元素 | stream.peek(System.out::println) |
5. 终结操作
终结操作会触发流水线的实际执行。
操作 | 方法签名 | 描述 | 示例 |
---|---|---|---|
遍历 | void forEach(Consumer<T>) | 对流中每个元素执行操作 | stream.forEach(System.out::println) |
收集 | <R, A> R collect(Collector<T, A, R>) | 将流转换为其他形式(如List, Set, Map) | stream.collect(Collectors.toList()) |
匹配 | boolean allMatch(Predicate<T>) boolean anyMatch(Predicate<T>) boolean noneMatch(Predicate<T>) | 检查流中元素是否全部/任一/没有一个匹配断言 | stream.allMatch(s -> s.length() > 3) |
计数 | long count() | 返回流中元素个数 | stream.count() |
查找 | Optional<T> findFirst() Optional<T> findAny() | 返回第一个/任意一个元素(在并行流中 findAny 效率更高) | stream.findFirst() |
归约 | Optional<T> reduce(BinaryOperator<T>) T reduce(T identity, BinaryOperator<T>) | 将流中元素反复结合,得到一个值(如求和、求最大最小值) | stream.reduce((a, b) -> a + b) stream.reduce(0, (a, b) -> a + b) |
6. 综合示例:把一切结合起来
假设我们有一个 Person
对象的列表,我们要进行一系列复杂查询。
// 数据源
List<Person> people = Arrays.asList(new Person("Alice", 25, "London"),new Person("Bob", 30, "New York"),new Person("Charlie", 20, "London"),new Person("Diana", 25, "Paris")
);// 目标:找出所有来自 London 的人,提取他们的名字,并按字母排序,最后放入一个新列表。
List<String> namesOfLondoners = people.stream() // 1. 获取流.filter(p -> "London".equals(p.getCity())) // 2. 过滤:只保留 London 的人.map(Person::getName) // 3. 映射:将 Person 对象映射为其名字 (String).sorted() // 4. 排序:按字母顺序排序.collect(Collectors.toList()); // 5. 收集:将结果转换为 ListSystem.out.println(namesOfLondoners); // 输出: [Alice, Charlie]// 更多例子:
// - 计算来自 London 的平均年龄
double averageAge = people.stream().filter(p -> "London".equals(p.getCity())).mapToInt(Person::getAge) // 专为 int 设计的流,有 sum(), average() 等便捷方法.average().orElse(0.0); // 如果没有任何元素,防止 NoSuchElementException// - 按城市对人进行分组
Map<String, List<Person>> peopleByCity = people.stream().collect(Collectors.groupingBy(Person::getCity));
// 结果: {New York=[Bob], London=[Alice, Charlie], Paris=[Diana]}// - 判断是否所有人都大于18岁
boolean allAdult = people.stream().allMatch(p -> p.getAge() > 18);
7. 并行流
将顺序流转换为并行流非常简单,通常只需将 .stream()
替换为 .parallelStream()
,或者在流中间调用 .parallel()
方法。
// 顺序流
long count = people.stream().filter(p -> p.getAge() > 20).count();// 并行流
long count = people.parallelStream().filter(p -> p.getAge() > 20).count();
原理:Stream API 在内部使用 Fork/Join 框架将任务拆分到多个线程上执行,最后将结果合并。
注意事项:
- 并非总是更快:线程的创建、管理和同步本身就有开销。对于小数据量,顺序流往往更快。
- 状态问题:确保传递给流操作(如
filter
,map
)的函数是无状态且无干扰的(不修改外部数据源),否则并行计算会产生竞态条件,导致错误结果。 - 适用场景:大数据集、处理耗时长的操作(如IO),且任务可独立并行执行时,性能提升最明显。
总结
Stream API 的核心优势:
- 声明式编程:代码更简洁、易读,你只需声明“要做什么”,而不是“如何去做”。
- 可组合性:操作可以像乐高积木一样灵活组合,构建复杂的处理流水线。
- 可并行化:无需编写复杂易错的多线程代码,就能轻松获得并行处理能力。
- 高效:得益于惰性求值,中间操作可以优化执行(如短路操作、循环合并)。
它彻底改变了 Java 程序员处理集合数据的方式,是编写现代、高效、简洁 Java 代码的必备工具。