第5讲:初识Stream API——告别显式迭代

34 阅读6分钟

[toc]

一、为什么需要Stream API:从传统迭代的痛点说起

在Java 8之前,我们对集合数据的处理主要依赖于显式迭代(如for循环、Iterator)。虽然功能上能够满足需求,但这种方式存在几个显著痛点:

传统迭代的代码示例:

List<User> users = getUserList();
List<String> adultNames = new ArrayList<>();

// 传统的for循环方式
for (User user : users) {
    if (user.getAge() >= 18) {  // 过滤条件
        adultNames.add(user.getName().toUpperCase());  // 数据处理
    }
}

// 还需要手动排序
Collections.sort(adultNames);

这种传统方式存在三大痛点

  1. 代码冗长:需要编写大量模板代码
  2. 难以并行化:手动实现并行处理复杂且容易出错
  3. 表达意图不清晰:代码更多地关注"如何做"而不是"做什么"

而Stream API的引入,让我们能够用更声明式的方式处理数据,专注于业务逻辑本身而非实现细节。

二、Stream的核心概念:到底是什么?

2.1 什么是Stream?

Stream(流)是Java 8中引入的一个新的抽象,它代表一个元素序列,支持顺序和并行聚合操作。可以将Stream理解为一个高级的迭代器,但有着本质的区别:

  • 不存储数据:Stream本身不存储数据,它只是数据源的一个视图
  • 函数式特性:对Stream的操作会产生新Stream,不会修改底层数据源
  • 延迟执行:中间操作都是延迟执行的,只有在终端操作时才会真正处理数据
  • 可消费性:Stream只能被消费一次,就像迭代器一样

2.2 Stream与集合的本质区别

为了更好理解Stream,我们通过下表对比Stream与集合的差异:

特性集合(Collection)流(Stream)
数据存储存储所有数据不存储数据,只是数据渠道
数据处理用户主动迭代(外部迭代)Stream API内部迭代
数据特性空间维度:存储数据时间维度:计算流程
遍历次数可多次遍历只能消费一次
处理方式命令式:如何迭代如何处理声明式:想要什么结果

关键理解:集合关注的是数据存储,而流关注的是数据计算

三、Stream的操作分类:中间操作与终端操作

Stream的操作分为两大类,理解这一分类对正确使用Stream至关重要。

3.1 中间操作(Intermediate Operations)

中间操作会返回一个新的Stream,并且总是延迟执行的。常见中间操作包括:

  • filter(Predicate):过滤不符合条件的元素
  • map(Function):将元素转换为另一种形式
  • sorted():对元素进行排序
  • distinct():去除重复元素
  • limit(long):限制元素数量

3.2 终端操作(Terminal Operations)

终端操作会触发实际计算,并产生结果或副作用。常见终端操作包括:

  • forEach(Consumer):对每个元素执行操作
  • collect(Collector):将流转换为集合或其他形式
  • reduce(BinaryOperator):将元素组合起来产生单个值
  • count():统计元素数量

重要特性:中间操作链不会立即执行,只有在调用终端操作时才会一起执行,这种设计优化了计算效率。

四、实战演练:从集合创建到流处理完整流程

4.1 创建Stream的多种方式

在实际开发中,我们可以通过多种方式创建Stream:

// 1. 从集合创建(最常用)
List<String> list = Arrays.asList("Java", "Python", "C++");
Stream<String> stream1 = list.stream();

// 2. 从数组创建
String[] array = {"Java", "Python", "C++"};
Stream<String> stream2 = Arrays.stream(array);

// 3. 使用Stream.of直接创建
Stream<String> stream3 = Stream.of("Java", "Python", "C++");

// 4. 创建无限流(用于生成序列)
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 1);

4.2 经典三部曲:filter -> map -> collect

让我们通过一个完整的例子来体验Stream的处理流程。假设我们有一个用户列表,需要找出年龄大于18岁的用户,提取他们的姓名并转换为大写,最后收集到List中

传统方式实现:

List<User> users = Arrays.asList(
    new User("Alice", 25),
    new User("Bob", 17),
    new User("Charlie", 30),
    new User("Diana", 16)
);

List<String> adultNames = new ArrayList<>();
for (User user : users) {
    if (user.getAge() > 18) {
        adultNames.add(user.getName().toUpperCase());
    }
}
Collections.sort(adultNames);

Stream API实现:

List<String> adultNames = users.stream()        // 1. 创建Stream
    .filter(user -> user.getAge() > 18)         // 2. 过滤:保留成年人
    .map(user -> user.getName().toUpperCase())  // 3. 映射:转换姓名格式
    .sorted()                                   // 4. 排序:按字母顺序
    .collect(Collectors.toList());              // 5. 收集:转换为List

代码分析:

  • stream():从users列表创建Stream
  • filter():中间操作,使用Predicate过滤元素
  • map():中间操作,使用Function转换元素
  • sorted():中间操作,对元素排序
  • collect():终端操作,触发计算并收集结果

4.3 更复杂的实战案例:电商订单处理

让我们看一个更接近真实业务的例子:

// 订单类
public class Order {
    private String orderId;
    private BigDecimal amount;
    private String status;
    // 构造器、getter、setter省略
}

List<Order> orders = Arrays.asList(
    new Order("001", new BigDecimal("100.50"), "COMPLETED"),
    new Order("002", new BigDecimal("250.00"), "PENDING"),
    new Order("003", new BigDecimal("75.30"), "COMPLETED"),
    new Order("004", new BigDecimal("300.00"), "COMPLETED")
);

// 使用Stream处理订单数据
BigDecimal totalCompletedAmount = orders.stream()
    .filter(order -> "COMPLETED".equals(order.getStatus()))  // 过滤已完成订单
    .map(Order::getAmount)                                   // 提取金额
    .reduce(BigDecimal.ZERO, BigDecimal::add);              // 求和

System.out.println("已完成订单总金额: " + totalCompletedAmount);

五、Stream API的优势总结

通过上面的例子,我们可以看到Stream API带来的显著优势:

5.1 代码简洁性提升

传统循环需要7-10行代码的逻辑,Stream API通常只需要3-5行,代码量减少约50%。

5.2 可读性大幅增强

Stream操作链清晰表达了业务意图,如"过滤→转换→收集",让代码更易于理解和维护。

5.3 并行化处理简单

.stream()改为.parallelStream()即可实现并行处理,大大简化了并发编程。

5.4 函数式编程优势

避免了外部迭代的副作用,代码更加安全可靠

六、注意事项与最佳实践

6.1 Stream使用注意事项

  1. Stream不可复用:一旦被消费,就不能再次使用
  2. 避免修改外部状态:Stream操作应该是无副作用的
  3. 合理使用并行流:数据量小或处理简单时,顺序流可能更高效

6.2 性能考量

// 不推荐的写法:多次操作同一数据源
Stream<String> stream1 = list.stream().filter(s -> s.length() > 3);
Stream<String> stream2 = list.stream().map(String::toUpperCase);

// 推荐的写法:操作链合并
List<String> result = list.stream()
    .filter(s -> s.length() > 3)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

七、总结

Stream API是Java 8函数式编程的核心特性之一,它让我们能够以更声明式、更简洁的方式处理集合数据。关键要点回顾:

  1. Stream不是集合,而是数据计算的流程
  2. 操作分为中间操作和终端操作,中间操作延迟执行
  3. 经典处理流程:filter(过滤)→ map(转换)→ collect(收集)
  4. 优势明显:代码简洁、可读性强、易于并行化

作为Java开发者,掌握Stream API不仅能让代码更加现代化,还能显著提升开发效率和代码质量。在接下来的章节中,我们将深入探讨Stream API更高级的特性和应用场景。

思考题:在你的项目中,哪些场景最适合用Stream API重构?尝试用Stream改写一个复杂的循环逻辑,体验代码简洁性的提升!

下期预告:第6讲:Stream的筛选、切片、映射与查找匹配


更多技术干货欢迎关注微信公众号“科威舟的AI笔记”~

【转载须知】:转载请注明原文出处及作者信息