Stream API

如果说前面的集合是”仓库”,那 Stream 就是流过仓库的”流水线”——它把数据当作一条流水线上的产品,经过一道道工序(过滤、转换、排序、收集),最终产出结果。Java 8 引入的 Stream API 是集合框架的一次革命:它让你从”怎么遍历”的命令式思维,跃迁到”想要什么”的声明式思维。

这一章是第四阶段的压轴。我们看 Stream 怎么把十几行 for 循环压缩成一行优雅的链式调用。

一、为什么需要 Stream

先看一个需求:从一堆学生里找出分数大于 80 的女生,按分数降序,取前 3 名的名字。

命令式写法(传统 for 循环):

List<Student> filtered = new ArrayList<>();
for (Student s : students) {
    if (s.getScore() > 80 && s.getGender() == Gender.FEMALE) {
        filtered.add(s);
    }
}
filtered.sort((a, b) -> b.getScore() - a.getScore());
List<String> result = new ArrayList<>();
for (int i = 0; i < Math.min(3, filtered.size()); i++) {
    result.add(filtered.get(i).getName());
}

声明式写法(Stream):

List<String> result = students.stream()
    .filter(s -> s.getScore() > 80)
    .filter(s -> s.getGender() == Gender.FEMALE)
    .sorted(Comparator.comparingInt(Student::getScore).reversed())
    .limit(3)
    .map(Student::getName)
    .collect(Collectors.toList());

同样是 7 步逻辑,Stream 把它压缩成一条”流水线”——每一道工序一目了然,没有中间变量、没有索引、没有循环模板。这就是声明式编程的魅力:描述”做什么”,而不是”怎么做”

二、Stream 是什么

首先要纠正一个常见误解:Stream 不是数据结构。它不存储数据,而是描述”对数据的操作序列”。

Stream 的关键特性:

  1. 不存储数据:它源于集合/数组/生成器,本身不存数据。
  2. 不修改源:操作 Stream 不会改变原集合。
  3. ** lazy 求值**:中间操作(filter/map 等)不会立即执行,直到终端操作触发。
  4. 一次性:一个 Stream 只能消费一次,用完即弃。
  5. 可能无限:Stream 可以表示无限序列(如 Stream.iterate),靠 limit 截断。
List<Integer> nums = List.of(1, 2, 3, 4, 5);
Stream<Integer> s = nums.stream();    // 创建流
// nums 没变;s 也没有"装"任何东西,只是描述了"对 nums 的操作"

三、创建 Stream

3.1 从集合

List<Integer> list = List.of(1, 2, 3);
Stream<Integer> s1 = list.stream();          // 顺序流
Stream<Integer> s2 = list.parallelStream();  // 并行流

3.2 从数组

String[] arr = {"A", "B", "C"};
Stream<String> s = Arrays.stream(arr);

3.3 静态工厂方法

Stream<Integer> s1 = Stream.of(1, 2, 3);
Stream<Integer> s2 = Stream.ofNullable(null);   // Java 9+,0 或 1 个元素
Stream<Double> s3 = Stream.generate(Math::random);   // 无限流
Stream<Integer> s4 = Stream.iterate(1, n -> n + 1);  // 无限流 1,2,3,...

// Java 9+ iterate 带终止条件
Stream<Integer> s5 = Stream.iterate(1, n -> n <= 100, n -> n + 1);   // 1..100

3.4 其他来源

// 字符串字符流
IntStream chars = "hello".chars();   // 104, 101, 108, 108, 111

// 文件行
Stream<String> lines = Files.lines(Path.of("file.txt"));

// 数值流范围
IntStream.range(1, 5);       // 1,2,3,4
IntStream.rangeClosed(1, 5); // 1,2,3,4,5

四、中间操作

中间操作返回 Stream,可以链式调用。它们是 lazy 的——不立即执行。

4.1 filter:过滤

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5).filter(n -> n > 2);   // 3,4,5

4.2 map:映射

Stream<String> s = Stream.of(1, 2, 3).map(n -> "第" + n + "名");   // 第1名,第2名,第3名

map 是一对一转换。如果转换函数本身返回 Stream,要用 flatMap 拍平。

4.3 flatMap:扁平化

List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4), List.of(5));
// 用 map 会得到 Stream<Stream<Integer>>,不好用
// 用 flatMap 拍平成 Stream<Integer>
Stream<Integer> flat = nested.stream().flatMap(List::stream);   // 1,2,3,4,5

flatMap 把”流中流”合并成一个流——类似”把多个抽屉里的东西全倒在桌上”。

经典应用:分词后扁平化。

List<String> sentences = List.of("hello world", "java stream");
List<String> words = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split(" ")))
    .collect(Collectors.toList());   // [hello, world, java, stream]

4.4 sorted:排序

Stream<Integer> s = Stream.of(3, 1, 2).sorted();                                  // 1,2,3
Stream<Integer> s2 = Stream.of(3, 1, 2).sorted(Comparator.reverseOrder());        // 3,2,1

4.5 distinct:去重

Stream<Integer> s = Stream.of(1, 2, 2, 3, 3, 3).distinct();   // 1,2,3

distinctequals 判断相等,相当于”流版 HashSet”。

4.6 peek:偷看

Stream<Integer> s = Stream.of(1, 2, 3)
    .peek(n -> System.out.println("处理: " + n));   // 调试用,不改变元素

peek 主要用于调试——在流水线中间插入一个”窥视”动作,看元素流过的状态。

4.7 limit / skip

Stream<Integer> s = Stream.of(1, 2, 3, 4, 5).limit(3);   // 1,2,3(取前 3)
Stream<Integer> s2 = Stream.of(1, 2, 3, 4, 5).skip(2);   // 3,4,5(跳过前 2)
Stream<Integer> s3 = Stream.of(1, 2, 3, 4, 5).skip(1).limit(2);   // 2,3

limit 在无限流上特别有用——Stream.iterate(1, n -> n+1).limit(10) 取前 10 个。

4.8 takeWhile / dropWhile(Java 9+)

Stream<Integer> s1 = Stream.of(1, 2, 3, 4, 5).takeWhile(n -> n < 4);   // 1,2,3
Stream<Integer> s2 = Stream.of(1, 2, 3, 4, 5).dropWhile(n -> n < 4);   // 4,5

takeWhile 在条件为 false 时停止;dropWhile 在条件为 false 时开始。它们和 filter 不同——filter 会遍历所有元素,而 takeWhile/dropWhile 是”遇到第一个不满足就停”。

五、终端操作

终端操作触发流水线执行,产生最终结果。没有终端操作,中间操作不会执行

5.1 forEach

Stream.of(1, 2, 3).forEach(System.out::println);

5.2 collect:收集

最强大的终端操作,把流元素收集成各种结果:

List<Integer> list = stream.collect(Collectors.toList());
Set<Integer> set = stream.collect(Collectors.toSet());
Map<String, Integer> map = stream.collect(Collectors.toMap(Object::toString, n -> n));
String joined = Stream.of("A", "B", "C").collect(Collectors.joining(", ", "[", "]"));   // [A, B, C]

详见下文 Collectors 一节。

5.3 reduce:归约

把流元素反复合并成一个值:

// 求和
int sum = Stream.of(1, 2, 3, 4, 5).reduce(0, Integer::sum);   // 15

// 求积
int product = Stream.of(1, 2, 3, 4).reduce(1, (a, b) -> a * b);   // 24

// 无初始值,返回 Optional
Optional<Integer> max = Stream.of(3, 1, 4, 1, 5).reduce(Integer::max);   // Optional[5]

reduce(identity, accumulator) 的过程:identity op e1 op e2 op e3 ...identity 是初始值(满足 identity op x == x 的”幺元”)。

5.4 count / min / max

long count = Stream.of(1, 2, 3).count();   // 3
Optional<Integer> min = Stream.of(3, 1, 2).min(Comparator.naturalOrder());   // Optional[1]
Optional<Integer> max = Stream.of(3, 1, 2).max(Comparator.naturalOrder());   // Optional[3]

min/max 返回 Optional——空流时返回 Optional.empty()

5.5 匹配:anyMatch / allMatch / noneMatch

boolean hasEven = Stream.of(1, 2, 3).anyMatch(n -> n % 2 == 0);   // true
boolean allEven = Stream.of(2, 4, 6).allMatch(n -> n % 2 == 0);    // true
boolean noEven = Stream.of(1, 3, 5).noneMatch(n -> n % 2 == 0);    // true

它们都是短路的——anyMatch 找到一个 true 就停,allMatch 找到一个 false 就停。

5.6 查找:findFirst / findAny

Optional<Integer> first = Stream.of(1, 2, 3).findFirst();   // Optional[1]
Optional<Integer> any = Stream.of(1, 2, 3).findAny();        // 任意一个(并行流更快)

六、Collectors:收集器大全

Collectorscollect 的”配方库”,提供各种收集方式。

6.1 toList / toSet / toCollection

stream.collect(Collectors.toList());                    // 转 List
stream.collect(Collectors.toSet());                     // 转 Set
stream.collect(Collectors.toCollection(LinkedList::new));   // 指定集合类型

💡 注意 Collectors.toList() 返回的是 ArrayList,但不能保证未来版本不变。要确保类型用 toCollection(ArrayList::new)

6.2 toMap

Map<String, Integer> map = students.stream()
    .collect(Collectors.toMap(Student::getName, Student::getScore));

:如果有重复 key,会抛 IllegalStateException。要解决,传第三个参数(合并函数):

Map<String, Integer> map = students.stream()
    .collect(Collectors.toMap(
        Student::getName,
        Student::getScore,
        (oldV, newV) -> newV   // 重复时取新值
    ));

6.3 groupingBy:分组

按某个属性分组,结果是 Map<键, List<元素>>

Map<Gender, List<Student>> byGender = students.stream()
    .collect(Collectors.groupingBy(Student::getGender));
// {FEMALE=[...], MALE=[...]}

二级分组:

Map<Gender, Map<Integer, List<Student>>> byGenderAndScore = students.stream()
    .collect(Collectors.groupingBy(Student::getGender,
             Collectors.groupingBy(s -> s.getScore() / 10)));

分组后做下游收集(不只是 toList):

Map<Gender, Long> countByGender = students.stream()
    .collect(Collectors.groupingBy(Student::getGender, Collectors.counting()));

Map<Gender, Double> avgScoreByGender = students.stream()
    .collect(Collectors.groupingBy(Student::getGender,
             Collectors.averagingInt(Student::getScore)));

Map<Gender, Optional<Student>> topByGender = students.stream()
    .collect(Collectors.groupingBy(Student::getGender,
             Collectors.maxBy(Comparator.comparingInt(Student::getScore))));

6.4 partitioningBy:分区

按布尔条件分成两组(true 一组,false 一组):

Map<Boolean, List<Student>> byPass = students.stream()
    .collect(Collectors.partitioningBy(s -> s.getScore() >= 60));
// {true=[及格的...], false=[不及格的...]}

6.5 joining:字符串拼接

String s1 = Stream.of("A", "B", "C").collect(Collectors.joining());        // ABC
String s2 = Stream.of("A", "B", "C").collect(Collectors.joining(", "));    // A, B, C
String s3 = Stream.of("A", "B", "C").collect(Collectors.joining(", ", "[", "]"));   // [A, B, C]

6.6 counting / summing / averaging / summarizing

long count = stream.collect(Collectors.counting());
int sum = stream.collect(Collectors.summingInt(Student::getScore));
double avg = stream.collect(Collectors.averagingInt(Student::getScore));
IntSummaryStatistics stats = stream.collect(Collectors.summarizingInt(Student::getScore));
// stats.getCount(), getSum(), getMin(), getAverage(), getMax()

summarizingInt 一次返回 count/sum/min/avg/max 全部统计——超方便。

七、数值流:IntStream / LongStream / DoubleStream

普通 Stream<Integer> 装箱开销大。处理数值时,用原始类型特化的 IntStream 等:

int sum = IntStream.of(1, 2, 3, 4, 5).sum();   // 15,无装箱

// 从普通流转数值流
int sum2 = students.stream().mapToInt(Student::getScore).sum();

// 范围
IntStream.range(1, 5).forEach(System.out::println);        // 1,2,3,4
IntStream.rangeClosed(1, 5).forEach(System.out::println);  // 1,2,3,4,5

// 统计
IntSummaryStatistics stats = IntStream.of(3, 1, 4, 1, 5).summaryStatistics();
System.out.println(stats.getAverage());   // 2.8
System.out.println(stats.getMax());       // 5

mapToInt / mapToLong / mapToDouble 把对象流变成数值流;boxed() 把数值流变回对象流。

八、并行流

.parallel()parallelStream() 让流并行执行——内部用 ForkJoinPool 自动切分任务。

long count = list.parallelStream()
    .filter(n -> isPrime(n))
    .count();

并行流的适用条件

  1. 数据量大(至少 1 万以上,否则切分开销大于收益)。
  2. 每个元素计算重(计算简单的话,并行收益不明显)。
  3. 操作无状态、无副作用(操作之间不依赖顺序)。
  4. 元素顺序无关紧要(或用 forEachOrdered 保序)。

注意事项

  • 并行流不保证顺序——forEach 的输出顺序可能乱。
  • 共享可变状态是灾难——forEach 里修改共享变量会数据竞争。
  • 终端操作 collect 会用合并函数合并各子任务结果——要确保合并满足结合律。
// ❌ 并行流 + 共享可变状态 = 灾难
ArrayList<Integer> result = new ArrayList<>();
list.parallelStream().forEach(result::add);   // 数据丢失、null、IndexOutOfBoundsException

// ✅ 用线程安全收集器
List<Integer> result = list.parallelStream().collect(Collectors.toList());

九、实战:用 Stream 处理学生成绩

Java · 在线运行

这个例子把 Stream API 的精华一网打尽:filtermapsortedlimitcollectgroupingBypartitioningByjoiningsummarizingIntreducemapToIntrangeClosed——日常 90% 的 Stream 操作都在这里了。

十、Stream 的设计哲学

Stream 的精妙在于它把”数据处理”抽象成了三个阶段:

源 → [中间操作] → 终端操作

中间操作是 lazy 的——它们不立即执行,而是”记下”要做的事。直到终端操作触发,整条流水线才一次性执行,每个元素”流过”所有工序。

这种设计有几个好处:

  1. 可优化:流水线知道全部操作,可以做”循环融合”——多个操作在一次遍历中完成,而不是多次遍历。
  2. 短路limitfindFirstanyMatch 等不需要遍历全部元素。
  3. 无限流:因为 lazy,Stream.iterate 可以表示无限序列,靠 limit 截断。

十一、本章小结

主题要点
Stream 本质不是数据结构,是操作序列;lazy、不修改源、一次性
创建Collection.stream()Stream.ofgenerateiterateArrays.stream
中间操作filter/map/flatMap/sorted/distinct/peek/limit/skip/takeWhile
终端操作forEach/collect/reduce/count/min/max/anyMatch/findFirst
CollectorstoList/toMap/groupingBy/partitioningBy/joining/summarizingInt
数值流IntStream/LongStream,避免装箱
reduce归约成单个值,需要幺元和结合函数
并行流.parallel(),大数据+重计算+无状态才用
并行流陷阱不能修改共享可变状态

结语:声明式的胜利

Stream API 是 Java 走向现代语言的关键一步。它把数据处理从”循环+临时变量+索引”的命令式泥潭,提升到了”过滤-映射-收集”的声明式优雅。

但 Stream 不是万能的:

  • 简单遍历用 for-each 更直观,不必强行 Stream。
  • 修改元素的场景,Stream 不如 for 循环方便(Stream 倡导无副作用)。
  • 性能敏感的小数据,Stream 的开销可能比 for 循环大。

掌握 Stream 的关键是:用”流水线”思维想问题——数据从源头流出,经过一道道工序,最终汇入结果容器。每道工序都是简洁的函数,组合起来却表达出强大的逻辑。

至此,第四阶段”集合框架”全部完成。我们走过了:体系总览、List、Set、Map、Queue/Deque、Collections 工具、Stream API。这套集合框架是 Java 程序员的核心武器库,几乎每个程序都离不开它们。

下一阶段,我们将进入 函数式编程——Lambda、函数式接口、方法引用。它们是 Stream API 的基石,也是 Java 现代编程风格的灵魂。