Stream 优化
Stream 写起来很爽,但用错了也会变慢。
这一节不讲语法,讲性能。什么样的写法快,什么样的写法慢,以及为什么。
减少中间操作
中间操作越少,管道越短,执行越快。
java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
long count = numbers.stream()
.filter(n -> n > 3) // 操作 1
.filter(n -> n % 2 == 0) // 操作 2:可以合并到上面
.count();
// ✅ 合并条件:少一次遍历
long count2 = numbers.stream()
.filter(n -> n > 3 && n % 2 == 0)
.count();
// ❌ 多次 map
List<String> result = numbers.stream()
.map(n -> n * 2) // 操作 1
.map(Object::toString) // 操作 2:可以合并
.collect(Collectors.toList());
// ✅ 合并 map
List<String> result2 = numbers.stream()
.map(n -> String.valueOf(n * 2))
.collect(Collectors.toList());善用短路操作
短路操作让 Stream 不必处理全部数据就返回结果:
java
// ❌ 不用短路:处理了全部数据
boolean allMatch = list.stream()
.filter(n -> n > 0) // 所有数据都过了一遍 filter
.allMatch(n -> n < 100);
// ✅ 用短路:找到第一个不匹配的立即返回
boolean allMatch2 = list.stream()
.allMatch(n -> n > 0 && n < 100);takeWhile / dropWhile(JDK 9+)
java
// takeWhile:取元素直到条件不满足就停止
List<Integer> result = Stream.of(1, 2, 3, 4, 1, 2)
.takeWhile(n -> n < 4)
.toList();
// [1, 2, 3](遇到 4 就停了,不再处理后面的 1, 2)
// dropWhile:跳过元素直到条件不满足
List<Integer> result2 = Stream.of(1, 2, 3, 4, 1, 2)
.dropWhile(n -> n < 4)
.toList();
// [4, 1, 2](跳过 1, 2, 3,遇到 4 之后全部保留)避免装箱开销
处理基本类型时,用专用流类型:
java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// ❌ 自动装箱:Integer 对象
long start = System.currentTimeMillis();
for (int i = 0; i < 100_000; i++) {
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b); // 每次都装箱/拆箱
}
// ✅ 避免装箱:IntStream
start = System.currentTimeMillis();
for (int i = 0; i < 100_000; i++) {
int sum = numbers.stream()
.mapToInt(Integer::intValue) // 转成 int[]
.sum();
}| 类型 | 专用流 | 避免装箱 |
|---|---|---|
| int | IntStream | ✅ |
| long | LongStream | ✅ |
| double | DoubleStream | ✅ |
| 其他 | Stream<Integer> | ❌ |
filter 后置
把能过滤的数据尽量提前过滤,减少后续操作的数据量:
java
// ❌ filter 靠后:先处理大量数据再过滤
list.stream()
.map(this::expensiveTransform)
.filter(result -> result.isValid())
.count();
// ✅ filter 靠前:先缩小数据量
list.stream()
.filter(this::isPromising) // 先过滤
.map(this::expensiveTransform) // 只处理少量数据
.filter(result -> result.isValid())
.count();使用正确的数据结构
有些数据结构天然适合并行操作:
java
// ArrayList:适合并行(可快速分割)
List<Integer> arrayList = new ArrayList<>(Arrays.asList(1, 2, 3));
arrayList.parallelStream()... // 好
// LinkedList:不适合并行(分割成本高)
List<Integer> linkedList = new LinkedList<>(Arrays.asList(1, 2, 3));
linkedList.parallelStream()... // 差,拆分 LinkedList 代价高
// HashSet:适合并行
Set<Integer> hashSet = new HashSet<>(Arrays.asList(1, 2, 3));
hashSet.parallelStream()... // 好
// Stream.of/Arrays.stream:适合并行
IntStream.range(0, 1_000_000)... // 好惰性求值的好处
中间操作的惰性带来了优化的可能:
java
// 只取前 10 个,即使数据有 1000 万条
// Stream 只会处理够找到 10 个结果所需的数据
list.stream()
.filter(n -> n > 0)
.sorted() // 这步不需要对全部数据排序
.limit(10)
.collect(Collectors.toList());
// 理解执行顺序
list.stream()
.limit(10) // ① 先 limit 限制数量
.filter(n -> n > 0) // ② 只处理前 10 个
.collect(Collectors.toList());并行 Stream 的条件
并行不是银弹。满足以下条件时并行才有效:
- 数据量大(> 1 万条)
- 操作是 CPU 密集或 IO 密集的
- 操作是无状态的
- 数据结构适合分割(ArrayList > 数组 > HashSet >> LinkedList)
java
// ✅ 并行有效:大数据量 + CPU 密集
IntStream.range(0, 10_000_000)
.parallel()
.filter(n -> isPrime(n))
.count();
// ❌ 并行无效:小数据量
Arrays.asList(1, 2, 3)
.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList());
// ❌ 并行无效:有状态
List<Integer> result = new ArrayList<>();
stream.parallelStream()
.forEach(n -> result.add(n)); // 有共享可变状态常用优化清单
| 优化项 | 做法 | 效果 |
|---|---|---|
| 合并 filter | filter(a && b) | 减少遍历次数 |
| 合并 map | map(f.andThen(g)) | 减少中间数组 |
| filter 靠前 | 先 filter 再 map | 减少处理量 |
| 用基本类型流 | IntStream vs Stream<Integer> | 避免装箱 |
| 用短路操作 | findFirst vs collect | 提前退出 |
| 选对数据结构 | ArrayList vs LinkedList | 并行效率 |
| 并行用对场景 | 大数据量 + 无状态 | 线性加速 |
记住:优化代码的第一步是测量。先跑个基准测试,确认慢在哪里,再针对性优化。
