java stream(建议收藏)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

前言

在 Java 开发中,处理集合数据(如 ListMap)时,开发者常常需要编写复杂的循环逻辑来完成过滤、排序、聚合等操作。然而,传统的循环方式不仅代码冗长,还容易引入逻辑错误。Java Stream 是 Java 8 引入的一个革命性特性,它通过函数式编程思想简化了集合操作,使代码更简洁、可读性更强。本文将从基础到进阶,结合实例讲解 Java Stream 的核心概念、常用操作及实战技巧,帮助开发者高效利用这一工具。


Java Stream 的核心概念

什么是 Stream?

Java Stream 是一种对集合数据进行 声明式操作 的接口,它通过一系列 管道化操作(如过滤、映射、排序等)将数据从输入源传输到终端,最终生成结果。

  • 声明式操作:开发者只需描述“做什么”,而非“如何做”,例如用 filter 指定筛选条件,而非手动遍历判断。
  • 惰性求值:Stream 的中间操作(如 filtermap)不会立即执行,只有遇到终端操作(如 collectforEach)时才会触发计算,这类似于 SQL 查询的执行逻辑。

如何创建 Stream?

Stream 可通过以下方式生成:

  1. 集合类Collection.stream()parallelStream()(并行流)。
  2. 数组Arrays.stream(array)
  3. 生成器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)

anyMatchallMatchnoneMatch 可用于条件判断,findFirstfindAny 可获取第一个匹配项。

示例:判断列表是否包含偶数

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() 可利用多线程加速处理,但需注意以下问题:

  1. 状态不可变:Stream 操作应避免修改外部变量。
  2. 顺序不可控:并行流的元素处理顺序可能与串行流不同。

示例:并行计算列表总和

int sum = IntStream.range(1, 1000)  
    .parallel()  
    .sum();  

常见错误与解决方案

  1. 多次调用终端操作:Stream 一旦被消费(如 collect),无法重复使用。

    Stream<String> s = Stream.of("a", "b");  
    s.collect(Collectors.toList()); // 正确  
    s.collect(Collectors.toList()); // 抛出 IllegalStateException  
    

    解决方式:重新生成 Stream 或使用中间变量存储结果。

  2. 副作用(Side Effects):在 mapfilter 中修改外部状态可能导致不可预测的结果。

    // 错误示例:试图通过 forEach 累加  
    int sum = 0;  
    numbers.stream().forEach(n -> sum += n); // 非线程安全且不符合函数式编程规范  
    

    替代方案:使用 reducesummingInt 等聚合操作。


案例实战:综合应用 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 融入日常开发,让代码更简洁、更优雅!

最新发布