实现Java集合迭代的高性能

Posted chszs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现Java集合迭代的高性能相关的知识,希望对你有一定的参考价值。

实现Java集合迭代的高性能

  • 2018.7.14
  • 版权声明:本文为博主chszs的原创文章,未经博主允许不得转载。

一、介绍

Java开发者经常会遇到处理集合(比如ArrayList、HashSet)的情况,Java 8也提供了Lambda表达式和Streaming API来简化集合相关的工作。在大多数应用场景下,无需考虑集合迭代的性能消耗。但是,在一些极端情况下,比如集合包含了上百万条记录的情况,这个时候集合迭代就需要选择正确的姿势,否则性能会较差。

使用JMH检查下面每段代码片段的运行时间。

二、forEach vs. C Style vs. Stream API

迭代是一个非常基本的功能,所有的编程语言都有简单的迭代语法,允许程序员在集合上运行迭代。Stream API可以通过Collections用非常直接的方式进行迭代。

public List<Integer> streamSingleThread(BenchMarkState state) 
    List<Integer> result = new ArrayList<>(state.testData.size());
    state.testData.stream().forEach(item -> 
        result.add(item);
    );
    return result;


public List<Integer> streamMultiThread(BenchMarkState state) 
    List<Integer> result = new ArrayList<>(state.testData.size());
    state.testData.stream().parallel().forEach(item -> 
        result.add(item);
    );
    return result;

使用forEach循环也非常简单:

public List<Integer> forEach(BenchMarkState state) 
    List<Integer> result = new ArrayList<>(state.testData.size());
    for(Integer item : state.testData) 
        result.add(item);
    
    return result;

C style方式的迭代其代码要冗长一些,但仍然非常紧凑:

public List<Integer> forCStyle(BenchMarkState state) 
    int size = state.testData.size();
    List<Integer> result = new ArrayList<>(size);
    for(int j = 0; j < size; j ++)
        result.add(state.testData.get(j));
    
    return result;

以上代码的性能评分如下:

BenchmarkModeCntScoreErrorUnits
TestLoopPerformance.forCStyleavgt20018.068± 0.074ms/op
TestLoopPerformance.forEachavgt20030.566± 0.165ms/op
TestLoopPerformance.streamMultiThreadavgt20079.433± 0.747ms/op
TestLoopPerformance.streamSingleThreadavgt20037.779± 0.485ms/op

对于C style方式的迭代,JVM只是简单地增加了一个整型变量,它直接从内存读值。这使它非常快。但forEach迭代则不同,根据Oracle官方文档,JVM必须把forEach转换为迭代器并为每个数据项调用hasNext()。这就是为什么forEach比C style迭代慢。

forEach文档:(https://docs.oracle.com/javase/1.5.0/docs/guide/language/foreach.html)

三、哪一种迭代的性能最高

我们定义测试数据:

@State(Scope.Benchmark)
public static class BenchMarkState 
    @Setup(Level.Trial)
    public void doSetup() 
        for(int i = 0; i < 500000; i++)
            testData.add(Integer.valueOf(i));
        
    
    @TearDown(Level.Trial)
    public void doTearDown() 
        testData = new HashSet<>(500000);
    
    public Set<Integer> testData = new HashSet<>(500000);

Java Set同时支持Stream API和forEach循环。根据前面的测试,如果我们把Set转换为ArrayList,看看性能是否有所提升。

public List<Integer> forCStyle(BenchMarkState state) 
    int size = state.testData.size();
    List<Integer> result = new ArrayList<>(size);
    Integer[] temp = (Integer[]) state.testData.toArray(new Integer[size]);
    for(int j = 0; j < size; j ++) 
        result.add(temp[j]);
    
    return result;

C style组合迭代的循环:

public List forCStyleWithIteration(BenchMarkState state)
int size = state.testData.size();
List result = new ArrayList<>(size);
Iterator iteration = state.testData.iterator();
for(int j = 0; j < size; j ++)
result.add(iteration.next());

return result;

forEach:

public List<Integer> forEach(BenchMarkState state) 
    List<Integer> result = new ArrayList<>(state.testData.size());
    for(Integer item : state.testData) 
        result.add(item);
    
    return result;

看起来代码简洁,但并不理想,因为初始化ArrayList比较消耗资源。

BenchmarkModeCntScoreErrorUnits
TestLoopPerformance.forCStyleavgt2006.013± 0.108ms/op
TestLoopPerformance.forCStyleWithIterationavgt2004.281± 0.049ms/op
TestLoopPerformance.forEachavgt2004.498± 0.026ms/op

HashMap (HashSet uses HashMap

结论

在集合Collections上使用Foreach和Stream API是非常便利的方法,写这样的代码也显得精炼。但要记住,当系统需要考虑性能和稳定性因素时,就应该改写这些循环。

以上是关于实现Java集合迭代的高性能的主要内容,如果未能解决你的问题,请参考以下文章

9.0对于java集合的迭代器的底层分析

数据结构 Java 版最全的 Java 集合框架入门手册

《高性能Java-集合》

《高性能Java-集合》

Java容器类总结

java 容器类总结