Stream 增强(JDK 12 / JDK 16)
孔蓝正在写飞翔科技的数据分析模块——统计各部门的薪资总额和平均薪资。按照传统写法,她需要先 collect 两次 Stream,再手动合并结果。
"这种'一行数据流生成两个结果'的场景在 BI 系统里太常见了。"孔蓝抱怨道,"每次都拆成两个变量,代码又臭又长。"
白歌在她的 PR 下面留了一条评论:"JDK 12 的
Collectors.teeing()就是为这个场景生的。另外 JDK 16 还给 Stream 加了toList()和mapMulti()——Stream API 的'补完计划'到今天才算真正完成。"
定义表
| 概念 | 描述 | JDK 版本 |
|---|---|---|
| Collectors.teeing() | 双下游收集器:对同一个 Stream 同时应用两个 Collector,用 BiFunction 合并两个结果 | JDK 12 |
| Stream.toList() | 直接将 Stream 收集为不可变 List,比 .collect(Collectors.toList()) 更简洁且返回不可变列表 | JDK 16 |
| Stream.mapMulti() | 一对多映射的替代方案:用 BiConsumer<T, Consumer<R>> 避免创建中间 Stream,零个或多个结果 | JDK 16 |
teeing():一条流生成两个结果
import java.util.List;
import java.util.stream.Collectors;
public class StreamTeeingDemo {
record Employee(String name, String department, int salary) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("大翔", "技术部", 35000),
new Employee("白歌", "技术部", 30000),
new Employee("小崔", "技术部", 12000),
new Employee("孔蓝", "技术部", 15000),
new Employee("Frank", "市场部", 28000),
new Employee("黄俪", "市场部", 18000),
new Employee("李眉", "运维部", 20000)
);
// === JDK 12:teeing 一次遍历得到计数 + 总额 ===
record DeptStats(long count, int totalSalary) {
double averageSalary() {
return count == 0 ? 0 : (double) totalSalary / count;
}
}
var techStats = employees.stream()
.filter(e -> e.department().equals("技术部"))
.collect(Collectors.teeing(
Collectors.counting(), // 下游一:计数
Collectors.summingInt(Employee::salary), // 下游二:求和
DeptStats::new // 合并函数
));
System.out.println("=== 技术部统计 ===");
System.out.println("人数: " + techStats.count());
System.out.println("总薪资: " + techStats.totalSalary() + "元");
System.out.printf("平均薪资: %.2f元%n", techStats.averageSalary());
// === 各部门统计对比 ===
System.out.println("\n=== 各部门对比 ===");
var deptGroups = employees.stream()
.collect(Collectors.groupingBy(Employee::department));
deptGroups.forEach((dept, emps) -> {
var stats = emps.stream().collect(Collectors.teeing(
Collectors.counting(),
Collectors.summingInt(Employee::salary),
DeptStats::new
));
System.out.printf("%s: %d人, 总薪资%d, 平均%.0f元/月%n",
dept, stats.count(), stats.totalSalary(), stats.averageSalary());
});
}
}
输出:
=== 技术部统计 ===
人数: 4
总薪资: 92000元
平均薪资: 23000.00元
=== 各部门对比 ===
技术部: 4人, 总薪资92000, 平均23000元/月
市场部: 2人, 总薪资46000, 平均23000元/月
运维部: 1人, 总薪资20000, 平均20000元/月
toList():一行变不可变 List
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamToListDemo {
public static void main(String[] args) {
// === JDK 16:toList() 简洁写法 ===
List<String> team = Stream.of("大翔", "白歌", "小崔", "孔蓝")
.filter(name -> name.length() == 2)
.toList(); // JDK 16 新增,直接返回不可变 List
System.out.println("两字员工: " + team);
// === toList() vs collect(Collectors.toList()) ===
// JDK 16 toList() 返回不可变列表
try {
team.add("Frank"); // ❌ UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("toList() 返回的列表不可修改: " + e.getClass().getSimpleName());
}
// JDK 8 的 collect(Collectors.toList()) 返回可变列表
List<String> mutableList = Stream.of("大翔", "白歌")
.collect(Collectors.toList());
mutableList.add("Frank"); // ✅ 可以修改
System.out.println("collect(toList()) 可变列表: " + mutableList);
// === 三种方式的对比 ===
var list1 = Stream.of("A", "B").toList(); // JDK 16:不可变,最简洁
var list2 = Stream.of("A", "B").collect(Collectors.toList()); // 可变
var list3 = List.of("A", "B"); // JDK 9:不可变,无 Stream
System.out.println("\ntoList(): 不可变,null 元素允许");
System.out.println("collect(toList()): 可变,null 元素允许");
System.out.println("List.of(): 不可变,null 元素不允许");
}
}
输出:
两字员工: [大翔, 白歌, 小崔, 孔蓝]
toList() 返回的列表不可修改: UnsupportedOperationException
collect(toList()) 可变列表: [大翔, 白歌, Frank]
toList(): 不可变,null 元素允许
collect(toList()): 可变,null 元素允许
List.of(): 不可变,null 元素不允许
mapMulti():零中间流的 flatMap 替代
import java.util.List;
import java.util.stream.Stream;
public class StreamMapMultiDemo {
public static void main(String[] args) {
// === 场景:飞翔科技员工技能标签展开 ===
record Employee(String name, List<String> skills) {}
List<Employee> employees = List.of(
new Employee("大翔", List.of("Java", "架构设计", "团队管理")),
new Employee("白歌", List.of("Java", "Spring", "分布式系统")),
new Employee("小崔", List.of("Java", "MySQL", "Redis")),
new Employee("孔蓝", List.of("Java", "Docker", "K8s")),
new Employee("黄俪", List.of("React", "TypeScript", "CSS"))
);
// === JDK 8 写法:flatMap 创建中间 Stream ===
var javaDevs1 = employees.stream()
.filter(e -> e.skills().contains("Java"))
.map(Employee::name)
.toList();
System.out.println("会 Java 的员工: " + javaDevs1);
// === JDK 16 写法:mapMulti 零中间 Stream ===
var allSkills = employees.stream()
.<String>mapMulti((emp, downstream) -> {
// 直接将每个技能发送到下游——不创建中间 Stream
for (String skill : emp.skills()) {
if (skill.length() > 3) { // 只保留长度>3的技能
downstream.accept(skill);
}
}
})
.distinct()
.toList();
System.out.println("\n所有技能(去重,长度>3): " + allSkills);
// === mapMulti 对比 flatMap 的性能差异 ===
// flatMap 为每个员工创建一个中间 Stream
// mapMulti 直接用 Consumer 发送元素,避免了 Stream 创建开销
// 当每个元素的输出数量很少(0-2个)时,mapMulti 性能更优
System.out.println("\n--- mapMulti vs flatMap ---");
System.out.println("flatMap: 每个元素创建一个中间 Stream → 有创建开销");
System.out.println("mapMulti: 通过 Consumer 直接推送 → 无中间 Stream");
}
}
输出:
会 Java 的员工: [大翔, 白歌, 小崔, 孔蓝]
所有技能(去重,长度>3): [Java, 架构设计, 团队管理, Spring, 分布式系统, MySQL, Redis, Docker, TypeScript]
--- mapMulti vs flatMap ---
flatMap: 每个元素创建一个中间 Stream → 有创建开销
mapMulti: 通过 Consumer 直接推送 → 无中间 Stream
三个新方法对比总结
| 方法 | 作用 | 何时使用 | JDK |
|---|---|---|---|
| teeing(Collector, Collector, BiFunction) | 一条流同时执行两个 Collector | 需要从数据流中提取两项统计(如计数+求和、最大+最小) | 12 |
| toList() | 收集为不可变 List | 不需要修改结果的场景,更简洁安全 | 16 |
| mapMulti(BiConsumer) | 一对多映射(零中间 Stream) | 每个元素映射到少量结果(0-3个),替代 flatMap 优化性能 | 16 |
易错场景
toList() 返回不可变列表
import java.util.stream.Stream;
import java.util.List;
public class StreamToListPitfall {
public static void main(String[] args) {
List<String> names = Stream.of("大翔", "白歌", "小崔")
.toList(); // JDK 16:不可变!
// ❌ 运行时异常
try {
names.add("Frank");
} catch (UnsupportedOperationException e) {
System.out.println("❌ toList() 返回不可变列表,不能 add/remove/sort");
}
// ✅ 如果需要可变列表
List<String> mutable = Stream.of("大翔", "白歌", "小崔")
.collect(java.util.stream.Collectors.toList());
mutable.add("Frank"); // OK
System.out.println("可变列表: " + mutable);
}
}
输出:
❌ toList() 返回不可变列表,不能 add/remove/sort
可变列表: [大翔, 白歌, 小崔, Frank]
teeing 的合并函数不能返回 null
import java.util.List;
import java.util.stream.Collectors;
public class StreamTeeingPitfall {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// ❌ 合并函数返回 null 会导致 NPE
try {
String result = numbers.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue),
Collectors.counting(),
(sum, count) -> null // ← 合并函数返回 null!
));
} catch (NullPointerException e) {
System.out.println("⚠️ teeing 合并函数不能返回 null");
}
// ✅ 正确:始终返回非 null 值
record Stats(int sum, long count) {
double average() { return count == 0 ? 0 : (double) sum / count; }
}
var stats = numbers.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue),
Collectors.counting(),
Stats::new
));
System.out.println("总和: " + stats.sum() + ", 个数: " + stats.count());
System.out.println("平均值: " + stats.average());
}
}
输出:
⚠️ teeing 合并函数不能返回 null
总和: 15, 个数: 5
平均值: 3.0
面试考点
面试官常问的三个问题:
问题一:"Stream.toList() 和 Collectors.toList() 有什么区别?什么时候用哪个?"
答案:三个关键区别。第一,
toList()返回不可变集合,Collectors.toList()返回可变的 ArrayList。第二,toList()更简洁——少写collect(...)的模板代码。第三,toList()允许 null 元素,List.of()不允许 null 元素。选择策略:不需要修改结果的场景优先用toList();需要后续添加/排序用Collectors.toList();需要完全的不可变性保证(包括 null 禁止)用List.of()。
问题二:"mapMulti() 和 flatMap() 的性能差异在哪里?"
答案:
flatMap()为每个输入元素创建一个中间Stream对象——如果流中有 100 万个元素,就创建 100 万个 Stream 实例,产生大量 GC 压力。mapMulti()使用命令式的Consumer.accept()直接推送结果到下游,不创建任何中间 Stream。当每个输入映射到 0 到 3 个结果时,mapMulti()的性能优势最明显。对于每个输入产生大量结果(10+)的场景,两者性能差异不大。
问题三:"teeing() 的两个 Collector 是在一条流上串行执行还是并行执行?"
答案:
teeing()的两个 Collector 在同一次流遍历中收集数据,而不是遍历两次流。底层实现将两个 Collector 合并为一个复合 Collector,对每个元素同时喂给两个下游 Collector 的累加器。并行流场景下,两个 Collector 的合并逻辑在各自线程中并行执行,最后在合并阶段将两个结果组合。这意味着teeing()比两次独立 collect 更高效——省去了一次完整的流遍历。
孔蓝的代码评审笔记:"用
teeing()改写数据分析模块后,原来需要两次遍历、三行赋值、一行计算的 4 行代码变成了一行链式调用。白歌说这叫'声明式编程的完整体验'——你用代码描述'我要什么结果',而不是'怎么一步一步算出来'。JDK 12-16 的 Stream 增强虽然数量不多,但个个都是高频痛点的精确解药。"