You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Java Stream API过滤在大数据集上比命令式循环更快?

Java Stream API与命令式循环过滤性能差异分析及优化方案

测试环境

# JMH version: 1.37
# VM version: JDK 22, OpenJDK 64-Bit Server VM, 22+36-FR
# VM invoker: /home/jack/.sdkman/candidates/java/22-amzn/bin/java

问题现象

我在基准测试中发现:当数据集元素数量达到1000或更多时,Java Stream API的过滤操作比命令式循环方式更快。小规模数据(1、10、100个元素)下命令式循环因低开销明显更快,但数据量超过1000后Stream API反超。想明确这种现象是否符合预期、背后原因是什么,以及如何修改命令式代码使其性能超过Stream API。

基准测试代码

package com.example;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Fork(1)
@Warmup(iterations = 20, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 20, time = 1, timeUnit = TimeUnit.SECONDS)
public class Main {

    @Param({"1", "10", "100", "1000", "100000", "1000000"})
    private int size;

    private List<Integer> list;

    @Setup(Level.Iteration)
    public void setup() {
        list = generateNumberedList(size);
    }

    @Benchmark
    public void streamFilter(Blackhole blackhole) {
        blackhole.consume(list.stream().filter(x -> x % 2 == 0).toList());
    }

    @Benchmark
    public void imperativeFilter(Blackhole blackhole) {
        List<Integer> result = new ArrayList<>(size);
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i) % 2 == 0) {
                result.add(list.get(i));
            }
        }
        blackhole.consume(result);
    }

    private List<Integer> generateNumberedList(int size) {
        List<Integer> list = new ArrayList<>(size);
        for (int i = 1; i <= size; i++) {
            list.add(i);
        }
        return list;
    }
}

测试结果

Benchmark               (size)  Mode  Cnt         Score         Error  Units
Main.imperativeFilter        1  avgt   20         9.941 ±       0.690  ns/op
Main.imperativeFilter       10  avgt   20        46.745 ±       1.653  ns/op
Main.imperativeFilter      100  avgt   20       409.121 ±       2.039  ns/op
Main.imperativeFilter     1000  avgt   20      4266.749 ±      18.419  ns/op
Main.imperativeFilter   100000  avgt   20    441107.842 ±    3836.223  ns/op
Main.imperativeFilter  1000000  avgt   20  12261798.155 ±  210891.798  ns/op
Main.streamFilter            1  avgt   20        45.916 ±       0.613  ns/op
Main.streamFilter           10  avgt   20        92.312 ±       1.068  ns/op
Main.streamFilter          100  avgt   20       553.842 ±       3.685  ns/op
Main.streamFilter         1000  avgt   20      3070.087 ±      17.789  ns/op
Main.streamFilter       100000  avgt   20    341999.158 ±    4990.941  ns/op
Main.streamFilter      1000000  avgt   20   9283982.963 ± 1436907.235  ns/op

原因分析

  1. Stream API的底层批量优化:JDK 22中Stream的toList()方法针对大数据量场景做了特殊处理,例如直接用数组存储结果,避免了ArrayList的扩容逻辑和冗余内存分配;同时基于Spliterator的遍历机制在处理ArrayList时,能更高效利用CPU缓存,减少边界检查的额外开销。
  2. JIT编译的深度优化:Stream的lambda表达式和流水线操作更容易被JIT编译器内联,在数据量较大时,JIT能识别并生成循环展开、向量化等优化代码,而原命令式循环的结构可能未触发同等程度的优化。
  3. 命令式代码的内存低效:原命令式代码用原列表的size初始化结果列表容量,但过滤偶数后结果最多只有原容量的一半,这会导致ArrayList分配过多内存,降低缓存命中率,在大数据量下这种低效会被放大。

命令式代码优化方案

优化点1:精准初始化结果列表容量

过滤偶数时,结果的最大容量为(size + 1) / 2,用该值初始化ArrayList,避免内存浪费:

List<Integer> result = new ArrayList<>((size + 1) / 2);

优化点2:缓存列表长度到局部变量

避免循环中重复调用list.size(),将长度缓存到局部变量,减少方法调用开销:

int n = list.size();
for (int i = 0; i < n; i++) {
    // 循环逻辑
}

优化点3:使用数组遍历替代列表get()

将ArrayList转换为数组后遍历,减少自动拆箱和列表访问的微小开销:

Integer[] arr = list.toArray(new Integer[0]);
for (Integer num : arr) {
    // 过滤逻辑
}

优化后的完整命令式方法

@Benchmark
public void optimizedImperativeFilter(Blackhole blackhole) {
    int n = list.size();
    List<Integer> result = new ArrayList<>((n + 1) / 2);
    Integer[] arr = list.toArray(new Integer[0]);
    for (Integer num : arr) {
        if (num % 2 == 0) {
            result.add(num);
        }
    }
    blackhole.consume(result);
}

额外优化:使用原始类型数组(消除装箱开销)

如果可以将输入列表改为int[]类型,能完全避免装箱拆箱的性能损耗,进一步提升性能:

// 修改测试类的状态和初始化逻辑
private int[] array;

@Setup(Level.Iteration)
public void setup() {
    array = generateNumberedArray(size);
}

private int[] generateNumberedArray(int size) {
    int[] arr = new int[size];
    for (int i = 0; i < size; i++) {
        arr[i] = i + 1;
    }
    return arr;
}

@Benchmark
public void primitiveImperativeFilter(Blackhole blackhole) {
    int n = array.length;
    int[] result = new int[(n + 1) / 2];
    int idx = 0;
    for (int num : array) {
        if (num % 2 == 0) {
            result[idx++] = num;
        }
    }
    blackhole.consume(result);
}

这些优化解决了原命令式代码的内存低效和微小开销问题,在大数据量下性能可超过Stream API。

内容的提问来源于stack exchange,提问作者Jack5000

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 22:17:33