Stream 是什么:为什么告别 for 循环

Java 8 之前处理集合,永远是 for 循环加 if 判断:遍历列表、过滤条件、转换格式、收集结果——四步下来几十行样板代码。Stream API 把这四步变成了一条流水线:数据像水流一样从源头进来,经过过滤、映射、排序,最后流进一个收集器。

举个例子:从一个用户列表里,找出年龄大于 18 的,提取他们的名字,按名字排序,转成 List。传统写法:

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

Stream 写法:

List<String> names = users.stream()
    .filter(u -> u.getAge() > 18)
    .map(User::getName)
    .sorted()
    .collect(Collectors.toList());

一行链式调用,意图清晰:过滤、映射、排序、收集。读代码的人一眼就知道这四步在干什么,不需要在 for 循环里翻逻辑。

Stream 不是数据结构,它不存储数据,也不改数据源。它是一个计算流水线——中间操作都是惰性的,只有终端操作触发时才真正执行。
本文核心知识点
  • 中间操作filter、map、sorted、distinct,返回新 Stream,可链式。
  • 终端操作collect、count、reduce、forEach,触发实际计算。
  • groupingBy一行代码完成分组统计。
  • parallelStream自动并行处理,大数据集提速。
Java Stream 示意:蓝色渐变背景上多条平行水流分支汇入中间漏斗过滤器,最终流入右下角收集桶
Stream 就是水流:多条支流汇入过滤器,筛选后的水最终流进收集桶。

中间操作:filter、map、sorted、distinct

中间操作返回一个新的 Stream,你可以接着链式调用。它们都是惰性的——写在那里不会执行,直到终端操作出现。

操作作用示例
filter按条件过滤元素.filter(u -> u.getAge() > 18)
map把元素转换成另一种类型.map(User::getName)
sorted排序.sorted(Comparator.comparing(User::getAge))
distinct去重.distinct()
limit取前 N 个.limit(10)
skip跳过前 N 个.skip(5)

map 是最常用的中间操作:它把 Stream<User> 变成 Stream<String>(提取名字),把 Stream<Order> 变成 Stream<BigDecimal>(提取金额)。每一层 map 做一次类型转换,像流水线的一道工序。

sorted 支持自定义比较器。按多个字段排序时用 Comparator 链式调用:Comparator.comparing(User::getAge).thenComparing(User::getName)——先按年龄排,年龄相同再按名字排。

终端操作:collect、reduce、count

终端操作是流水线的终点——它触发整个计算,产出一个结果。没有终端操作,前面写的所有 filter、map 都不会执行。

// 收集成 List
List<String> names = users.stream()
    .map(User::getName)
    .collect(Collectors.toList());

// 计数
long adultCount = users.stream()
    .filter(u -> u.getAge() >= 18)
    .count();

// 求和
int totalAge = users.stream()
    .mapToInt(User::getAge)
    .sum();

// reduce:把所有元素归约成一个值
Optional<User> oldest = users.stream()
    .reduce((a, b) -> a.getAge() > b.getAge() ? a : b);

collect 是最灵活的终端操作——配合 Collectors 工具类,可以收集成 List、Set、Map,也可以分组、拼接字符串、求平均值。它是 Stream 世界里的瑞士军刀。

reduce 用于把流里所有元素合并成一个值。求和、求最大值、拼接字符串都可以用 reduce 实现。它返回 Optional,因为流可能是空的。

collect 分组:groupingBy 实战

业务里经常要分组统计:按部门分组算人数、按状态分组算订单量、按城市分组列用户。传统做法是建一个 Map,遍历列表,put 进去 if else 判断。Stream 的 groupingBy 一行搞定。

// 按部门分组
Map<String, List<User>> byDept = users.stream()
    .collect(Collectors.groupingBy(User::getDept));

// 按部门分组,只统计每组人数
Map<String, Long> countByDept = users.stream()
    .collect(Collectors.groupingBy(User::getDept, Collectors.counting()));

// 按部门分组,统计每组平均年龄
Map<String, Double> avgAgeByDept = users.stream()
    .collect(Collectors.groupingBy(
        User::getDept,
        Collectors.averagingInt(User::getAge)
    ));

// 拼接成逗号分隔字符串
String names = users.stream()
    .map(User::getName)
    .collect(Collectors.joining(", "));

groupingBy 的第二个参数是下游收集器——你可以在分组之后再做计数、求和、取最大值。这比写一个 Map 加 for 循环清晰太多。

另一个高频用法是 toMap:把 List 转成 Map,key 是 id,value 是对象本身。做查找时 O(1) 命中,不用每次遍历。

Lambda 与方法引用

Stream 的参数都是 Lambda 表达式——匿名函数,不需要定义类。u -> u.getAge() > 18 就是一个 Lambda:入参 u,返回一个布尔值。

当 Lambda 只是调用一个已有的方法时,可以用方法引用简写:User::getName 等价于 u -> u.getName()。方法引用有四种形式:

// 静态方法引用
Integer::parseInt

// 实例方法引用(特定对象)
user::getName

// 实例方法引用(任意对象)
String::length

// 构造方法引用
User::new

方法引用让代码更短、意图更清晰。看到 User::getName 就知道是在提取 name 属性,不需要读 u -> u.getName() 再理解一遍。