Stream API 增强(JDK 9)
飞翔科技的季度数据报告会上,小崔展示了他用 Stream API 写的一段数据分析代码。
"我把所有员工的绩效分排序,取出前80%——" 小崔突然卡住了,"呃,Stream 好像没法中途停止筛选..."
// 想取"所有得分≥80的员工",但后面还有不及格的,不想全遍历 scores.stream() .filter(s -> s >= 80) // 即使遇到60分也不会停,会继续遍历 .collect(toList());白歌走到白板前:"JDK 8 的
filter是全量遍历,不管条件后续是否成立。但 JDK 9 新增了四个方法,专治各种'中途停止'、'跳过头部'、'按条件终止'的场景。这就是我们今天要讲的takeWhile、dropWhile、iterate增强版和ofNullable。"
定义表
| 方法 | 签名 | 功能 | 适用场景 | 等效 JDK 8 替代 |
|---|---|---|---|---|
takeWhile(Predicate) | 从头开始取元素,直到第一个不满足条件的元素停止 | 有序流截取前缀 | filter(但 filter 不会中途停止) | |
dropWhile(Predicate) | 从头开始丢弃元素,直到第一个不满足条件的元素开始保留 | 跳过前缀 | 无直接等效(需自己写循环) | |
iterate(seed, Predicate, UnaryOperator) | 生成无限流,直到条件为 false 停止 | 有终止条件的无限流 | iterate(seed, f) + limit(n)(但 limit 不知道精确数量) | |
ofNullable(T) | 0 或 1 个元素的 Stream,null 时为空流 | 避免 NPE 的流操作 | value == null ? Stream.empty() : Stream.of(value) |
Mermaid 流程图:takeWhile 与 filter 的行为差异
takeWhile — 按条件截取前缀
import java.util.List;
import java.util.stream.Collectors;
public class TakeWhileDemo {
public static void main(String[] args) {
// === 场景:飞翔科技员工按绩效分排序(已从高到低排好) ===
List<Integer> scores = List.of(98, 95, 92, 88, 76, 65, 55, 48);
// --- filter:全量遍历,取所有 ≥ 80 的 ---
List<Integer> filtered = scores.stream()
.filter(s -> s >= 80)
.collect(Collectors.toList());
System.out.println("filter: " + filtered); // 76被跳过,但继续查了65、55...
// --- takeWhile:从第一个不满足的开始停止 ---
List<Integer> taken = scores.stream()
.takeWhile(s -> s >= 80)
.collect(Collectors.toList());
System.out.println("takeWhile: " + taken); // 遇到76就停了,65/55不会再被检查
// === 实战:按时间排序的日志,取今天的 ===
List<String> logs = List.of(
"2026-06-14 09:00 启动", "2026-06-14 10:00 运行",
"2026-06-13 23:00 备份", "2026-06-13 22:00 告警"
);
var todayLogs = logs.stream()
.takeWhile(log -> log.startsWith("2026-06-14"))
.collect(Collectors.toList());
System.out.println("\n今日日志: " + todayLogs);
}
}
输出:
filter: [98, 95, 92, 88]
takeWhile: [98, 95, 92, 88]
今日日志: [2026-06-14 09:00 启动, 2026-06-14 10:00 运行]
关键理解:
takeWhile不是"筛选满足条件的",而是"从开头取,直到碰到不满足的停止"。对无序流(如HashSet)行为不可预测,所以只应在有序流上使用。
dropWhile — 按条件丢弃前缀
import java.util.List;
import java.util.stream.Collectors;
public class DropWhileDemo {
public static void main(String[] args) {
// === 场景:过滤掉排名靠前的已知数据,看后面的 ===
List<Integer> scores = List.of(100, 100, 99, 85, 72, 68);
// dropWhile:丢弃开头所有满足条件的,从第一个不满足的开始保留
List<Integer> notTop = scores.stream()
.dropWhile(s -> s >= 99) // 丢弃 100, 100, 99
.collect(Collectors.toList());
System.out.println("dropWhile(≥99): " + notTop);
// === 实战:解析 CSV 跳过空行前缀 ===
List<String> csvLines = List.of(
"", "", "", // 文件头部的空行
"编号,姓名,部门",
"1001,大翔,技术部",
"1002,白歌,技术部"
);
var data = csvLines.stream()
.dropWhile(String::isBlank) // 跳过开头的空行
.collect(Collectors.toList());
System.out.println("\n跳过空行前缀后:");
data.forEach(System.out::println);
// === 实战:去掉日志中的 DEBUG 前缀 ===
List<String> logs = List.of(
"DEBUG - 初始化", "DEBUG - 加载配置",
"INFO - 服务启动", "WARN - 内存不足", "ERROR - 连接失败"
);
var important = logs.stream()
.dropWhile(line -> line.startsWith("DEBUG"))
.collect(Collectors.toList());
System.out.println("\n重要日志:");
important.forEach(System.out::println);
}
}
输出:
dropWhile(≥99): [85, 72, 68]
跳过空行前缀后:
编号,姓名,部门
1001,大翔,技术部
1002,白歌,技术部
重要日志:
INFO - 服务启动
WARN - 内存不足
ERROR - 连接失败
iterate 增强版 — 带终止条件的无限流
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class IterateDemo {
public static void main(String[] args) {
// === JDK 8 iterate:需要 limit 截断,但不知道具体次数 ===
// 问题:想生成 "所有 ≤ 100 的偶数",但不知道 limit 填多少
var jdk8Even = Stream.iterate(0, n -> n + 2)
.limit(51) // 硬编码!如果改起始值,limit 就不对了
.collect(Collectors.toList());
System.out.println("JDK8(硬编码limit=51): " + jdk8Even.size() + "个元素");
// === JDK 9 iterate(seed, hasNext, next) ===
var jdk9Even = Stream.iterate(0, n -> n <= 100, n -> n + 2)
.collect(Collectors.toList());
System.out.println("JDK9(条件终止): " + jdk9Even.size() + "个元素");
System.out.println("末尾几个: " +
jdk9Even.stream().skip(Math.max(0, jdk9Even.size()-3))
.collect(Collectors.toList()));
// === 实战:生成飞翔科技工资等级的阈值 ===
// S级(≥10年) → A级(≥5年) → B级(≥2年) → C级(≥0年)
var salaryBands = Stream.iterate(
new int[]{10, 50000}, // seed: {年限, 工资}
band -> band[0] >= 0, // hasNext: 年限≥0
band -> new int[]{band[0] - 5, band[1] - 10000} // next: 降级
)
.map(band -> String.format("%d年 → ¥%d", band[0], band[1]))
.collect(Collectors.toList());
System.out.println("\n工资等级:");
salaryBands.forEach(System.out::println);
}
}
输出:
JDK8(硬编码limit=51): 51个元素
JDK9(条件终止): 51个元素
末尾几个: [96, 98, 100]
工资等级:
10年 → ¥50000
5年 → ¥40000
0年 → ¥30000
ofNullable — null 安全的流元素
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class OfNullableDemo {
public static void main(String[] args) {
// === 场景:飞翔科技员工查询,null 代表离职 ===
String emp1 = "大翔";
String emp2 = null; // 已离职
String emp3 = "白歌";
// --- JDK 8:繁琐的三目运算符 ---
Stream<String> s1 = emp1 != null ? Stream.of(emp1) : Stream.empty();
Stream<String> s2 = emp2 != null ? Stream.of(emp2) : Stream.empty();
Stream<String> s3 = emp3 != null ? Stream.of(emp3) : Stream.empty();
List<String> jdk8Result = Stream.concat(Stream.concat(s1, s2), s3)
.collect(Collectors.toList());
System.out.println("JDK 8: " + jdk8Result);
// --- JDK 9:Stream.ofNullable 一行搞定 ---
List<String> jdk9Result = Stream.of(
Stream.ofNullable(emp1),
Stream.ofNullable(emp2),
Stream.ofNullable(emp3)
)
.flatMap(s -> s)
.collect(Collectors.toList());
System.out.println("JDK 9: " + jdk9Result);
// === 实战:批量查询,null → 跳过 ===
List<String> names = Arrays.asList("大翔", null, "白歌", null, "小崔");
var validNames = names.stream()
.flatMap(Stream::ofNullable) // null 自动过滤
.collect(Collectors.toList());
System.out.println("\n有效员工: " + validNames);
// === ofNullable 与 Optional.stream() 的对比 ===
// 先转 Optional 再转 Stream
var fromOptional = names.stream()
.map(Optional::ofNullable)
.flatMap(Optional::stream) // JDK 9 Optional.stream()
.collect(Collectors.toList());
System.out.println("Optional.stream(): " + fromOptional);
}
}
输出:
JDK 8: [大翔, 白歌]
JDK 9: [大翔, 白歌]
有效员工: [大翔, 白歌, 小崔]
Optional.stream(): [大翔, 白歌, 小崔]
四大增强方法综合实战
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamEnhanceRealWorld {
public static void main(String[] args) {
// === 飞翔科技:从混合日志中提取有效错误信息 ===
List<String> rawLogs = List.of(
"DEBUG 初始化组件", // ← 跳过
"DEBUG 加载配置", // ← 跳过
"INFO 服务启动",
"WARN 内存使用率85%",
"WARN 磁盘使用率90%",
"ERROR 数据库连接超时",
"ERROR 缓存服务不可达",
"DEBUG GC回收完成" // ← 后面的 DEBUG 也要跳过
);
// 需求:提取所有 WARN 和 ERROR 日志,但跳过开头的 DEBUG
// 同时排除 null(日志可能丢失)
List<String> rawWithNull = List.of(
"DEBUG 初始化组件",
null, // ← 日志丢失
"INFO 服务启动",
"WARN 内存使用率85%",
"ERROR 数据库连接超时",
null // ← 日志丢失
);
var criticalLogs = rawWithNull.stream()
.flatMap(Stream::ofNullable) // JDK 9: 过滤 null
.dropWhile(line -> line.startsWith("DEBUG")) // JDK 9: 跳过头部 DEBUG
.filter(line -> line.startsWith("WARN") || line.startsWith("ERROR"))
.collect(Collectors.toList());
System.out.println("关键告警日志:");
criticalLogs.forEach(System.out::println);
// === 使用 iterate 生成序列号 ===
var serialNumbers = Stream.iterate(
"FLY-2026-0001",
s -> s.compareTo("FLY-2026-0010") <= 0,
s -> {
int num = Integer.parseInt(s.substring(9));
return String.format("FLY-2026-%04d", num + 1);
}
)
.collect(Collectors.toList());
System.out.println("\n飞翔科技工单编号: " + serialNumbers);
}
}
输出:
关键告警日志:
WARN 内存使用率85%
ERROR 数据库连接超时
飞翔科技工单编号: [FLY-2026-0001, FLY-2026-0002, ..., FLY-2026-0010]
易错场景
8.1 takeWhile 在无序流上的不可预测行为
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class TakeWhileUnorderedPitfall {
public static void main(String[] args) {
Set<Integer> scores = new HashSet<>(List.of(98, 60, 95, 55, 88));
// ⚠️ takeWhile 在无序集合(HashSet)上的行为不可预测!
var taken = scores.stream()
.takeWhile(s -> s >= 80)
.collect(Collectors.toList());
System.out.println("HashSet takeWhile(≥80): " + taken);
// ✅ 正确:先排序,保证有序
var sortedTaken = scores.stream()
.sorted()
.takeWhile(s -> s >= 80)
.collect(Collectors.toList());
System.out.println("先排序后 takeWhile: " + sortedTaken);
}
}
8.2 iterate 的 hasNext 在 seed 上评估
import java.util.stream.Stream;
public class IterateHasNextPitfall {
public static void main(String[] args) {
// hasNext 在每次生成之前对当前值进行判断
// 如果 seed 本身就不满足条件,流为空
var empty = Stream.iterate(0, n -> n < 0, n -> n + 1)
.count();
System.out.println("seed不满足hasNext: " + empty + "个元素");
// 正常情况:hasNext 判断"上一个元素"是否还满足条件
var list = Stream.iterate(0, n -> n < 5, n -> n + 1)
.collect(java.util.stream.Collectors.toList());
System.out.println("0到4: " + list);
}
}
面试考点
问题一:"takeWhile 和 filter 的区别?"
filter遍历所有元素,对每个元素判断条件;takeWhile从头开始取,遇到第一个不满足条件的元素就停止,不再检查后续元素。filter适合从整个数据集中筛选,takeWhile适合在已排序的数据中截取前缀。
问题二:"dropWhile 的典型使用场景?"
跳过数据流开头不需要的部分。常见场景:跳过日志文件中的 DEBUG 级别前缀、跳过 CSV 文件开头的空行、跳过排名列表中已经处理过的顶级玩家。
dropWhile在遇到第一个不满足条件的元素后立即停止丢弃,保留该元素及之后的所有元素。
问题三:"Stream.ofNullable(null) 返回什么?"
返回一个空 Stream(元素数量为 0),与
Stream.empty()等价。这与Stream.of(null)不同——后者会创建一个包含单个 null 元素的流,后续操作可能抛出 NullPointerException。
小崔学完这四个方法后在工位上感叹:"以前处理排序后截断、跳过头部无效数据、生成有上限的序列——全都要自己写循环或者用 limit 硬编码。JDK 9 这四个方法像是专门给数据处理场景设计的'手术刀',精准又优雅。"