乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

Stream API 基础

本章定位:Stream API 是 JDK 8 中最具生产力的特性,它让 Java 开发者可以用声明式、函数式的方式处理集合数据。本章将深入 Stream 的创建、中间操作、终端操作,以及最重要的特性——惰性求值。


1. 场景引入:孔蓝的困惑

飞翔科技的校招生孔蓝第一天入职就被分配了一个任务:从公司 500 名员工数据中,找出技术部薪资最高的 3 名 90 后员工。她用 JDK 7 的写法折腾了半天:

// JDK 7:近 20 行代码,可读性差
List<Employee> techEmps = new ArrayList<>();
for (Employee e : employees) {
    if ("技术部".equals(e.getDepartment())) {
        techEmps.add(e);
    }
}
Collections.sort(techEmps, new Comparator<Employee>() {
    @Override
    public int compare(Employee a, Employee b) {
        return Double.compare(b.getSalary(), a.getSalary());
    }
});
List<Employee> top3 = new ArrayList<>();
for (int i = 0; i < Math.min(3, techEmps.size()); i++) {
    top3.add(techEmps.get(i));
}

架构师白歌 review 代码时叹了口气,写下了 JDK 8 版本:

// JDK 8:一行流式处理,语义清晰
List<Employee> top3 = employees.stream()
    .filter(e -> "技术部".equals(e.getDepartment()))
    .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
    .limit(3)
    .collect(Collectors.toList());

孔蓝看呆了——这就是 Stream API 的威力。


2. Stream 的概念

2.1 什么是 Stream

Stream(流) 不是数据结构,不存储数据。它是对数据源(集合、数组、I/O 通道等)的函数式操作管道,用于对数据进行聚合、转换和计算。

特性说明类比
不存储数据Stream 只是一个"视图",操作完后数据仍在原集合中像 SQL 的视图,不存储数据
函数式操作通过 Lambda 传递行为,不修改数据源类似 RxJava / React 的响应式流
惰性求值中间操作不会立即执行,只有终端操作触发时才执行像 Builder 模式,build() 时才真正构建
一次性Stream 只能被消费一次,消费完后需重新创建像 Iterator,遍历完就没了
内部迭代迭代逻辑由 Stream 库内部管理,开发者只声明"做什么"数据库执行计划

2.2 Stream vs 集合


3. Stream 的创建

3.1 创建方式总览

创建方式语法适用场景
从集合collection.stream()最常用,List/Set 等
从数组Arrays.stream(array)基本类型数组、对象数组
直接创建Stream.of(T... values)少量已知元素
无限流(迭代)Stream.iterate(seed, unaryOp)数学序列、无限序列
无限流(生成)Stream.generate(supplier)随机数、常量流
从文件Files.lines(path)按行读取文件
从字符串"abc".chars()字符处理
BuilderStream.builder().add().build()动态构建

3.2 代码示例

// 1. 从集合创建(最常用)
List<Employee> list = getEmployees();
Stream<Employee> stream1 = list.stream();           // 顺序流
Stream<Employee> parallelStream = list.parallelStream();  // 并行流

// 2. 从数组创建
String[] names = {"大翔", "白歌", "小崔", "孔蓝"};
Stream<String> stream2 = Arrays.stream(names);
Stream<String> subStream = Arrays.stream(names, 1, 3);  // 只取 [白歌, 小崔]

// 3. 直接创建
Stream<Integer> stream3 = Stream.of(1, 2, 3, 4, 5);
Stream<String> stream4 = Stream.of("A", "B", "C");

// 4. 无限流(需要用 limit() 截断)
Stream<Integer> naturalNumbers = Stream.iterate(0, n -> n + 1);  // 0, 1, 2, 3, ...
Stream<Double> randomValues = Stream.generate(Math::random);       // 随机数流

// 5. 从文件读取
try (Stream<String> lines = Files.lines(Paths.get("employees.txt"))) {
    lines.filter(line -> line.contains("技术部"))
         .forEach(System.out::println);
}

注意:Stream.iterate() 和 Stream.generate() 创建的无限流必须配合 limit() 使用,否则会无限执行下去。


4. 中间操作(Intermediate Operations)

中间操作返回一个新的 Stream,多个中间操作可以链式调用形成管道。中间操作不会立即执行,它们只是记录下操作步骤,等终端操作触发时才真正执行——这称为惰性求值。

4.1 中间操作速查表

操作方法签名功能是否有状态
filterStream<T> filter(Predicate<T>)按条件过滤无状态
map<R> Stream<R> map(Function<T,R>)元素一对一转换无状态
flatMap<R> Stream<R> flatMap(Function<T,Stream<R>>)元素一对多展平无状态
distinctStream<T> distinct()去重(基于 equals)有状态
sortedStream<T> sorted() / sorted(Comparator)排序有状态
peekStream<T> peek(Consumer<T>)调试/窥视(不改变元素)无状态
limitStream<T> limit(long n)截取前 n 个有状态(短路)
skipStream<T> skip(long n)跳过前 n 个有状态

4.2 filter:过滤

// 筛选技术部薪资 > 15000 的员工
List<Employee> techHighSalary = employees.stream()
    .filter(e -> "技术部".equals(e.getDepartment()))
    .filter(e -> e.getSalary() > 15000)
    .collect(Collectors.toList());

4.3 map:转换

// 提取所有员工的姓名列表
List<String> names = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toList());

// 计算每个员工的年薪
List<Double> annualSalaries = employees.stream()
    .map(emp -> emp.getSalary() * 12)
    .collect(Collectors.toList());

4.4 flatMap:展平

flatMap 将每个元素映射为一个 Stream,然后将所有 Stream 连接成一个 Stream——即"展平"操作。

// 场景:每个员工有多个技能标签,获取所有技能的去重列表
class Employee {
    private List<String> skills;  // ["Java", "Spring", ...]
    public List<String> getSkills() { return skills; }
}

// 错误做法:map 得到的是 Stream<List<String>>
Stream<List<String>> skillLists = employees.stream().map(Employee::getSkills);

// 正确做法:flatMap 得到 Stream<String>
List<String> allSkills = employees.stream()
    .flatMap(emp -> emp.getSkills().stream())
    .distinct()
    .collect(Collectors.toList());

4.5 sorted:排序

// 按薪资降序排列
List<Employee> bySalary = employees.stream()
    .sorted((a, b) -> Double.compare(b.getSalary(), a.getSalary()))
    .collect(Collectors.toList());

// 使用 Comparator 工具方法(推荐)
List<Employee> bySalary2 = employees.stream()
    .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
    .collect(Collectors.toList());

// 多级排序:先按部门,再按薪资
List<Employee> multiSort = employees.stream()
    .sorted(Comparator.comparing(Employee::getDepartment)
                      .thenComparing(Comparator.comparingDouble(Employee::getSalary).reversed()))
    .collect(Collectors.toList());

4.6 peek:调试利器

// 使用 peek 观察管道中每一步的数据变化
List<String> result = employees.stream()
    .peek(e -> System.out.println("原始: " + e))
    .filter(e -> e.getSalary() > 15000)
    .peek(e -> System.out.println("  过滤后: " + e))
    .map(Employee::getName)
    .peek(name -> System.out.println("    映射后: " + name))
    .collect(Collectors.toList());

注意:peek 主要用于调试,不应依赖其副作用。在并行流中,peek 的执行顺序可能是非确定性的。

4.7 limit / skip:分页与截取

// 获取薪资最高的前 3 名
List<Employee> top3 = employees.stream()
    .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
    .limit(3)
    .collect(Collectors.toList());

// 分页:第 2 页,每页 10 条
int page = 2;
int pageSize = 10;
List<Employee> page2 = employees.stream()
    .skip((page - 1) * pageSize)
    .limit(pageSize)
    .collect(Collectors.toList());

5. 终端操作(Terminal Operations)

终端操作会触发 Stream 管道的执行,返回最终结果(非 Stream 类型)。执行后,Stream 被消费,不能再次使用。

5.1 终端操作速查表

操作返回类型功能短路?
forEachvoid遍历每个元素否
collectR(集合/Map/字符串等)将流收集为集合否
toArrayObject[] / T[]转换为数组否
reduceOptional<T> / T归约/聚合否
countlong计数否
anyMatchboolean存在任一匹配是
allMatchboolean全部匹配是
noneMatchboolean无匹配是
findFirstOptional<T>找第一个是
findAnyOptional<T>找任意一个(并行友好)是
min/maxOptional<T>最小/最大值否

5.2 短路操作(Short-Circuiting)

短路操作是指不需要处理完整个流就能得出结果的操作。一旦找到满足条件的元素,立即终止后续元素的处理。

短路操作的强大在于:配合 limit(),可以避免不必要的计算。例如从无限流中取前 10 个元素:

// 生成前 100 个斐波那契数对
Stream.iterate(new long[]{0, 1}, f -> new long[]{f[1], f[0] + f[1]})
    .limit(100)           // 短路:只处理 100 个
    .map(f -> f[0])
    .forEach(System.out::println);

// findFirst 短路示例
Optional<Employee> firstHighSalary = employees.stream()
    .filter(e -> e.getSalary() > 50000)
    .findFirst();         // 找到第一个就停止,不会处理后续元素

6. 惰性求值详解

6.1 什么是惰性求值

惰性求值(Lazy Evaluation) 是指中间操作不会立即执行,而是等到终端操作被调用时,才一次性"拉取"数据并执行整个管道。这是 Stream 性能优势的核心。

6.2 惰性求值实验

public class LazyEvaluationDemo {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("大翔", "白歌", "小崔", "孔蓝");

        System.out.println("=== 实验1:无终端操作 ===");
        Stream<String> stream = names.stream()
            .filter(name -> {
                System.out.println("  filter 正在处理: " + name);
                return name.length() > 1;
            })
            .map(name -> {
                System.out.println("  map 正在处理: " + name);
                return name.toUpperCase();
            });
        System.out.println("Stream 管道已构建,但没有任何输出!");

        System.out.println("\n=== 实验2:添加终端操作后 ===");
        // 此时才真正执行
        List<String> result = stream.collect(Collectors.toList());
        System.out.println("结果: " + result);
    }
}
运行输出:
=== 实验1:无终端操作 ===
Stream 管道已构建,但没有任何输出!

=== 实验2:添加终端操作后 ===
  filter 正在处理: 大翔
  map 正在处理: 大翔
  filter 正在处理: 白歌
  map 正在处理: 白歌
  filter 正在处理: 小崔
  map 正在处理: 小崔
  filter 正在处理: 孔蓝
  map 正在处理: 孔蓝
结果: [大翔, 白歌, 小崔, 孔蓝]

关键发现:终端操作 collect 调用之前,filter 和 map 的代码根本没有执行!

6.3 惰性求值的执行模型

惰性求值带来的优势:

  1. 减少遍历次数:不需要为每个中间操作创建临时集合,一次遍历完成所有操作
  2. 短路优化:limit(3) 配合 sorted() 可能只需找到"最小的 3 个"而无需完整排序
  3. 无限流支持:可以在无限流上使用 limit() 安全截断
  4. 内存友好:中间结果不会存储在临时集合中(有状态操作如 sorted 除外)

7. 完整代码示例

示例一:飞翔科技员工综合查询

场景:小崔需要实现一个员工综合查询功能,支持按部门、薪资范围、入职时间进行筛选,结果按薪资排序,支持分页。

import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class EmployeeQueryDemo {
    public static void main(String[] args) {
        // 准备测试数据
        List<Employee> employees = Arrays.asList(
            new Employee("大翔", 50000, "管理层", LocalDate.of(2015, 3, 1), 
                Arrays.asList("管理", "战略", "Java")),
            new Employee("白歌", 35000, "技术部", LocalDate.of(2017, 6, 15),
                Arrays.asList("Java", "Spring", "架构", "分布式")),
            new Employee("小崔", 18000, "技术部", LocalDate.of(2021, 7, 1),
                Arrays.asList("Java", "MyBatis", "SQL")),
            new Employee("孔蓝", 12000, "技术部", LocalDate.of(2023, 9, 1),
                Arrays.asList("Python", "AI", "Django")),
            new Employee("老刘", 9000, "后勤部", LocalDate.of(2018, 1, 10),
                Arrays.asList("采购", "物流")),
            new Employee("阿杰", 25000, "产品部", LocalDate.of(2019, 4, 20),
                Arrays.asList("需求分析", "Axure", "数据分析")),
            new Employee("小美", 15000, "产品部", LocalDate.of(2022, 11, 5),
                Arrays.asList("UI设计", "Figma", "用户体验"))
        );

        System.out.println("=== 1. 技术部薪资 > 15000 的员工,按薪资降序 ===");
        employees.stream()
            .filter(e -> "技术部".equals(e.getDepartment()))
            .filter(e -> e.getSalary() > 15000)
            .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
            .forEach(e -> System.out.printf("  %s, 薪资: %.0f%n", e.getName(), e.getSalary()));

        System.out.println("\n=== 2. 各部门的员工名单(flatMap 展平技能) ===");
        Map<String, List<String>> deptEmployees = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.mapping(Employee::getName, Collectors.toList())
            ));
        deptEmployees.forEach((dept, names) -> 
            System.out.printf("  %s: %s%n", dept, names));

        System.out.println("\n=== 3. 所有技能去重列表 ===");
        List<String> allSkills = employees.stream()
            .flatMap(e -> e.getSkills().stream())
            .distinct()
            .sorted()
            .collect(Collectors.toList());
        System.out.println("  " + allSkills);

        System.out.println("\n=== 4. 薪资分页(第1页,每页3条) ===");
        List<Employee> page1 = employees.stream()
            .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
            .limit(3)
            .collect(Collectors.toList());
        page1.forEach(e -> System.out.printf("  %s: %.0f%n", e.getName(), e.getSalary()));

        System.out.println("\n=== 5. 统计信息 ===");
        long techCount = employees.stream()
            .filter(e -> "技术部".equals(e.getDepartment()))
            .count();
        boolean hasHighSalary = employees.stream()
            .anyMatch(e -> e.getSalary() > 40000);
        boolean allHaveName = employees.stream()
            .allMatch(e -> e.getName() != null && !e.getName().isEmpty());
        System.out.printf("  技术部人数: %d%n", techCount);
        System.out.printf("  存在薪资 > 40000: %b%n", hasHighSalary);
        System.out.printf("  所有员工都有姓名: %b%n", allHaveName);

        System.out.println("\n=== 6. 首个薪资最低的员工 ===");
        employees.stream()
            .min(Comparator.comparingDouble(Employee::getSalary))
            .ifPresent(e -> System.out.printf("  最低薪资: %s, %.0f%n", e.getName(), e.getSalary()));
    }
}
运行输出:
=== 1. 技术部薪资 > 15000 的员工,按薪资降序 ===
  白歌, 薪资: 35000
  小崔, 薪资: 18000

=== 2. 各部门的员工名单(flatMap 展平技能) ===
  技术部: [白歌, 小崔, 孔蓝]
  产品部: [阿杰, 小美]
  后勤部: [老刘]
  管理层: [大翔]

=== 3. 所有技能去重列表 ===
  [AI, Axure, Django, Figma, Java, MyBatis, Python, SQL, Spring, UI设计, 分布式, 战略, 数据分析, 架构, 物流, 用户体验, 管理, 采购, 需求分析]

=== 4. 薪资分页(第1页,每页3条) ===
  大翔: 50000
  白歌: 35000
  阿杰: 25000

=== 5. 统计信息 ===
  技术部人数: 3
  存在薪资 > 40000: true
  所有员工都有姓名: true

=== 6. 首个薪资最低的员工 ===
  最低薪资: 老刘, 9000

示例二:无限流与数据生成

场景:白歌需要生成一批测试用的员工 ID,格式为 "EMP-YYYY-NNNN"(年份 + 序号)。

import java.util.stream.Stream;

public class InfiniteStreamDemo {
    public static void main(String[] args) {
        // 生成员工编号:EMP-2024-0001 格式
        System.out.println("=== 生成前 10 个员工编号 ===");
        Stream.iterate(1, n -> n + 1)
            .limit(10)
            .map(n -> String.format("EMP-2024-%04d", n))
            .forEach(System.out::println);

        // 斐波那契数列
        System.out.println("\n=== 前 10 个斐波那契数 ===");
        Stream.iterate(new long[]{0, 1}, f -> new long[]{f[1], f[0] + f[1]})
            .limit(10)
            .map(f -> f[0])
            .forEach(n -> System.out.print(n + " "));
        System.out.println();

        // 生成随机薪资数据(18000 ~ 50000)
        System.out.println("\n=== 生成 5 个随机薪资 ===");
        Stream.generate(() -> Math.random() * 32000 + 18000)
            .limit(5)
            .map(salary -> Math.round(salary * 100) / 100.0)
            .forEach(salary -> System.out.printf("  %.2f%n", salary));

        // 验证短路:在无限流中找第一个满足条件的
        System.out.println("\n=== 在无限流中找第一个 > 45000 的值 ===");
        Stream.generate(() -> Math.random() * 32000 + 18000)
            .peek(v -> System.out.printf("  检查: %.2f%n", v))
            .filter(v -> v > 45000)
            .findFirst()
            .ifPresent(v -> System.out.printf("找到! %.2f%n", v));
    }
}
运行输出:
=== 生成前 10 个员工编号 ===
EMP-2024-0001
EMP-2024-0002
EMP-2024-0003
EMP-2024-0004
EMP-2024-0005
EMP-2024-0006
EMP-2024-0007
EMP-2024-0008
EMP-2024-0009
EMP-2024-0010

=== 前 10 个斐波那契数 ===
0 1 1 2 3 5 8 13 21 34 

=== 生成 5 个随机薪资 ===
  27654.32
  19456.78
  45123.90
  22109.45
  38976.11

=== 在无限流中找第一个 > 45000 的值 ===
  检查: 23012.45
  检查: 34219.87
  检查: 46201.33
找到! 46201.33

8. 易错场景

反例一:Stream 重复消费

小崔不小心对一个 Stream 调用了两次终端操作:

// ❌ 错误:Stream 只能消费一次
Stream<Employee> stream = employees.stream().filter(e -> e.getSalary() > 15000);
List<Employee> list1 = stream.collect(Collectors.toList());   // 第一次消费 OK
List<Employee> list2 = stream.collect(Collectors.toList());   // IllegalStateException

报错信息:

java.lang.IllegalStateException: stream has already been operated upon or closed
// ✅ 正确:使用 Supplier 包装,每次需要时重新创建
Supplier<Stream<Employee>> streamSupplier = () -> 
    employees.stream().filter(e -> e.getSalary() > 15000);
List<Employee> list1 = streamSupplier.get().collect(Collectors.toList());
List<Employee> list2 = streamSupplier.get().collect(Collectors.toList());

反例二:惰性求值陷阱——副作用在中间操作中

// ❌ 错误:期望 filter 中的打印语句执行,但忘了加终端操作
employees.stream()
    .filter(e -> {
        System.out.println("处理: " + e.getName());  // 这行永远不会执行!
        return e.getSalary() > 15000;
    });
System.out.println("完成");
// 输出只有 "完成",没有员工信息
// ✅ 正确:添加终端操作触发执行
employees.stream()
    .filter(e -> {
        System.out.println("处理: " + e.getName());
        return e.getSalary() > 15000;
    })
    .collect(Collectors.toList());  // 必须有终端操作

反例三:在 Stream 操作中修改外部集合

// ❌ 错误:在 filter 的 Lambda 中修改外部集合——副作用 + 非线程安全
List<Employee> highSalary = new ArrayList<>();
employees.stream()
    .filter(e -> {
        if (e.getSalary() > 15000) {
            highSalary.add(e);  // 副作用!不应在 Stream 操作中修改外部状态
        }
        return true;
    })
    .collect(Collectors.toList());
// ✅ 正确:使用 collect 收集结果
List<Employee> highSalary = employees.stream()
    .filter(e -> e.getSalary() > 15000)
    .collect(Collectors.toList());

反例四:sorted + limit 的性能误解

// ❌ 错误认知:以为 sorted() 会完整排序所有元素
// 实际上 JDK 的 sorted() 实现确实会完整排序(对于 Collection.stream()),
// 但如果数据量很大,limit(3) 无法帮助 sorted() 减少工作量
employees.stream()
    .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
    .limit(3)  // limit 只是从排序结果中截取,排序仍然处理了所有元素
    .collect(Collectors.toList());
// ✅ 优化:如果只需要 Top N,考虑使用优先队列(PriorityQueue)
// 或者在使用并行流时,sorted 的实现会更高效
// 对于极大数据集,手动实现 Top N 算法比 sorted().limit(N) 更高效

9. 面试考点

Q1:Stream 和集合有什么区别?

  1. 存储:集合是数据结构,存储数据;Stream 不存储数据,只是数据管道的抽象。
  2. 修改:集合可以增删改元素;Stream 不修改数据源,只产生结果。
  3. 迭代:集合是外部迭代(for-each 由调用者控制);Stream 是内部迭代(由库控制,支持并行优化)。
  4. 遍历:集合可多次遍历;Stream 只能消费一次。
  5. 求值:集合是立即的(eager);Stream 的中间操作是惰性的(lazy)。

Q2:什么是惰性求值?有什么好处?

惰性求值是指中间操作不会立即执行,而是等到终端操作触发时才一次性执行。好处:

  1. 减少遍历次数:多个操作合并为一次遍历
  2. 支持短路:findFirst() + filter() 找到第一个匹配项就停止
  3. 支持无限流:可以在无限流上用 limit() 安全截断
  4. 内存友好:不产生中间临时集合

Q3:中间操作和终端操作的区别是什么?

中间操作返回 Stream 类型,可以链式调用,且是惰性的(不触发执行);终端操作返回非 Stream 的结果(void、集合、Optional、boolean 等),调用后会触发整个管道的执行并消费 Stream。常见中间操作:filter、map、sorted。常见终端操作:collect、forEach、reduce、findFirst。

Q4:flatMap 和 map 的区别?

map 是一对一映射,将 T 转换为 R;flatMap 是一对多映射 + 展平,将 T 转换为 Stream<R> 然后将多个流合并为一个流。典型场景:将嵌套集合展平(如 List<List<String>> → List<String>),或处理 Optional(Optional.flatMap())。

Q5:short-circuiting(短路)操作有哪些?

  • limit(n):截取前 n 个后停止处理后续元素
  • findFirst() / findAny():找到第一个/任意一个匹配项后停止
  • anyMatch() / allMatch() / noneMatch():匹配到足以判断结果后停止

短路操作配合惰性求值,可以大幅减少不必要的元素处理。


白歌总结:"Stream API 让 Java 开发者第一次拥有了媲美 SQL 的数据操作表达能力。记住三个核心原则:不修改数据源、惰性求值、内部迭代。掌握了这三点,你就真正理解了 Stream。"

上一页
Lambda 表达式
下一页
Stream API 高级详解