java stream(建议收藏)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观
前言
在 Java 开发中,处理集合数据(如 List
或 Map
)时,开发者常常需要编写复杂的循环逻辑来完成过滤、排序、聚合等操作。然而,传统的循环方式不仅代码冗长,还容易引入逻辑错误。Java Stream 是 Java 8 引入的一个革命性特性,它通过函数式编程思想简化了集合操作,使代码更简洁、可读性更强。本文将从基础到进阶,结合实例讲解 Java Stream 的核心概念、常用操作及实战技巧,帮助开发者高效利用这一工具。
Java Stream 的核心概念
什么是 Stream?
Java Stream 是一种对集合数据进行 声明式操作 的接口,它通过一系列 管道化操作(如过滤、映射、排序等)将数据从输入源传输到终端,最终生成结果。
- 声明式操作:开发者只需描述“做什么”,而非“如何做”,例如用
filter
指定筛选条件,而非手动遍历判断。 - 惰性求值:Stream 的中间操作(如
filter
、map
)不会立即执行,只有遇到终端操作(如collect
、forEach
)时才会触发计算,这类似于 SQL 查询的执行逻辑。
如何创建 Stream?
Stream 可通过以下方式生成:
- 集合类:
Collection.stream()
或parallelStream()
(并行流)。 - 数组:
Arrays.stream(array)
。 - 生成器:
Stream.generate()
或Stream.iterate()
。
示例代码:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream1 = names.stream(); // 从集合生成
int[] numbers = {1, 2, 3, 4};
IntStream stream2 = Arrays.stream(numbers); // 从数组生成
Stream 的常用操作:从简单到复杂
中间操作(Intermediate Operations)
中间操作返回一个新的 Stream,不会触发计算,主要用于定义数据处理的逻辑链。
1. 过滤(filter)
通过 filter(Predicate<T> predicate)
方法筛选符合条件的元素。
比喻:就像用筛子过滤沙子,只保留符合孔径的颗粒。
示例:过滤出偶数
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// 结果:[2, 4]
2. 映射(map)
通过 map(Function<T, R> mapper)
将每个元素转换为另一种类型或格式。
比喻:如同将一叠纸币兑换成硬币,元素的形态改变但数量不变。
示例:将字符串转换为大写
List<String> names = Arrays.asList("apple", "banana");
List<String> upperCaseNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// 结果:["APPLE", "BANANA"]
3. 扁平化(flatMap)
当处理嵌套结构(如列表的列表)时,flatMap
可将多层结构“压平”为单层 Stream。
比喻:想象把多个书架上的书全部取出,堆叠成一个大书堆。
示例:合并多本书的章节列表
List<List<String>> chapters = Arrays.asList(
Arrays.asList("Chapter1", "Chapter2"),
Arrays.asList("ChapterA", "ChapterB")
);
Stream<String> allChapters = chapters.stream()
.flatMap(List::stream);
// 最终结果包含 "Chapter1", "Chapter2", "ChapterA", "ChapterB"
终端操作(Terminal Operations)
终端操作会触发 Stream 的实际计算,并返回结果或产生副作用。
1. 收集结果(collect)
通过 collect(Collector<? super T, A, R> collector)
将 Stream 结果收集到集合或对象中。
示例:将 Stream 转换为 Set
Set<Integer> uniqueNumbers = numbers.stream()
.filter(n -> n > 2)
.collect(Collectors.toSet());
// 结果:{3, 4, 5}(无序)
2. 归约(reduce)
reduce
方法用于将 Stream 元素合并为单一值,例如求和或求最大值。
示例:计算列表总和
Optional<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);
// 结果:Optional[15]
3. 遍历(forEach)
直接对每个元素执行操作,但需注意避免副作用(如修改外部状态)。
示例:打印元素
names.stream()
.forEach(System.out::println);
进阶特性:排序、匹配与分组
排序(sorted)
通过 sorted()
方法对 Stream 进行排序,默认按自然顺序,也可自定义比较器。
示例:按字符串长度排序
List<String> sortedNames = names.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
// 若 names 是 ["apple", "banana"],结果为 ["apple", "banana"]
匹配与查找(match、find)
anyMatch
、allMatch
和 noneMatch
可用于条件判断,findFirst
或 findAny
可获取第一个匹配项。
示例:判断列表是否包含偶数
boolean hasEven = numbers.stream()
.anyMatch(n -> n % 2 == 0);
// 若 numbers 包含 2,则返回 true
分组与分区(groupingBy、partitioningBy)
使用 Collectors.groupingBy
可按条件对元素分组,partitioningBy
可将数据分为两个分区。
示例:按性别分组用户
Map<Gender, List<User>> groupedByGender = users.stream()
.collect(Collectors.groupingBy(User::getGender));
性能优化与常见误区
并行流(Parallel Streams)
通过 parallelStream()
可利用多线程加速处理,但需注意以下问题:
- 状态不可变:Stream 操作应避免修改外部变量。
- 顺序不可控:并行流的元素处理顺序可能与串行流不同。
示例:并行计算列表总和
int sum = IntStream.range(1, 1000)
.parallel()
.sum();
常见错误与解决方案
-
多次调用终端操作:Stream 一旦被消费(如
collect
),无法重复使用。Stream<String> s = Stream.of("a", "b"); s.collect(Collectors.toList()); // 正确 s.collect(Collectors.toList()); // 抛出 IllegalStateException
解决方式:重新生成 Stream 或使用中间变量存储结果。
-
副作用(Side Effects):在
map
或filter
中修改外部状态可能导致不可预测的结果。// 错误示例:试图通过 forEach 累加 int sum = 0; numbers.stream().forEach(n -> sum += n); // 非线程安全且不符合函数式编程规范
替代方案:使用
reduce
或summingInt
等聚合操作。
案例实战:综合应用 Stream
假设需要从订单列表中筛选出金额大于 100 元的订单,计算总销售额,并按用户分组统计订单数。
List<Order> orders = ... // 假设已有订单数据
Map<User, Long> userOrderCount = orders.stream()
.filter(order -> order.getAmount() > 100)
.collect(
Collectors.groupingBy(
Order::getUser,
Collectors.counting()
)
);
long totalSales = orders.stream()
.filter(order -> order.getAmount() > 100)
.mapToDouble(Order::getAmount)
.sum();
结论
Java Stream 通过声明式编程简化了集合操作,其核心在于“管道化”数据处理与惰性求值机制。开发者需掌握中间操作与终端操作的搭配,并注意避免常见误区。随着项目复杂度的提升,合理使用 Stream 可显著提升代码的可维护性与性能。建议读者通过实际项目练习,逐步掌握 Stream 的高级特性(如自定义 Collector
),从而在 Java 开发中更高效地处理数据流。
通过本文的学习,希望读者能够将 Java Stream 融入日常开发,让代码更简洁、更优雅!