Java性能优化-HashMap遍历的7种方式及性能测试对比

项目合作/软件定制请私信留言,公众号:霸道的程序猿 / 2024-07-12 / 原文

场景

Java中使用JMH(Java Microbenchmark Harness 微基准测试框架)进行性能测试和优化:

https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/131723751

参考以上性能测试工具的使用。下面针对Java中对HashMap的7种遍历方式做性能测试。

注:

博客:
https://blog.csdn.net/badao_liumang_qizhi

实现

Java中HashMap遍历的方式

HashMap遍历,从大的方向来说,可分为以下4类:

1、迭代器(Iterator)方式遍历

2、For Each方式遍历

3、Lambda表达式遍历(JDK 1.8+)

4、Streams API遍历(JDK1.8+)

但每种类型下又有不同的实现方式,因此具体的遍历⽅式又可以分为以下7种:

1. 使⽤迭代器(Iterator)EntrySet 的⽅式进⾏遍历;

    public static void EntrySetForEach(){
        //创建并赋值HashMap
        Map<Integer,String> map = new HashMap<>();
        map.put(1,"公众号");
        map.put(2,"霸道的程序猿");
        map.put(3,"测试1");
        map.put(4,"测试2");
        map.put(5,"测试3");

        //遍历
        Iterator<Map.Entry<Integer,String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry<Integer,String> entry = iterator.next();
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }

2. 使⽤迭代器(Iterator)KeySet 的⽅式进⾏遍历;

    public static void KeySetForEach(){
        Map<Integer,String> map = new HashMap<>();
        map.put(1,"公众号");
        map.put(2,"霸道的程序猿");
        map.put(3,"测试1");
        map.put(4,"测试2");
        map.put(5,"测试3");
        Iterator<Integer> iterator = map.keySet().iterator();
        while(iterator.hasNext()){
            Integer key = iterator.next();
            System.out.println(key);
            System.out.println(map.get(key));
        }
    }

3. 使⽤ For Each EntrySet 的⽅式进⾏遍历;

    public static void ForEachEntrySet(){
        Map<Integer,String> map = new HashMap<>();
        map.put(1,"公众号");
        map.put(2,"霸道的程序猿");
        map.put(3,"测试1");
        map.put(4,"测试2");
        map.put(5,"测试3");
        for (Map.Entry<Integer,String> entry:map.entrySet()) {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }

4. 使⽤ For Each KeySet 的⽅式进⾏遍历;

    public static void ForEachKeySet(){
        Map<Integer,String> map = new HashMap<>();
        map.put(1,"公众号");
        map.put(2,"霸道的程序猿");
        map.put(3,"测试1");
        map.put(4,"测试2");
        map.put(5,"测试3");
        for(Integer key:map.keySet()){
            System.out.println(key);
            System.out.println(map.get(key));
        }
    }

5. 使⽤ Lambda 表达式的⽅式进⾏遍历;

    public static void LambdaForEach(){
        Map<Integer,String> map = new HashMap<>();
        map.put(1,"公众号");
        map.put(2,"霸道的程序猿");
        map.put(3,"测试1");
        map.put(4,"测试2");
        map.put(5,"测试3");
        map.forEach((key,value)->{
            System.out.println(key);
            System.out.println(value);
        });
    }

6. 使⽤ Streams API 单线程的⽅式进⾏遍历;

    public static void StreamSingle(){
        Map<Integer,String> map = new HashMap<>();
        map.put(1,"公众号");
        map.put(2,"霸道的程序猿");
        map.put(3,"测试1");
        map.put(4,"测试2");
        map.put(5,"测试3");
        map.entrySet().stream().forEach((entry)->{
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }

7. 使⽤ Streams API 多线程的⽅式进⾏遍历。

    public static void StreamMul(){
        Map<Integer,String> map = new HashMap<>();
        map.put(1,"公众号");
        map.put(2,"霸道的程序猿");
        map.put(3,"测试1");
        map.put(4,"测试2");
        map.put(5,"测试3");
        map.entrySet().parallelStream().forEach((entry)->{
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }

Java中7种遍历HashMap方式性能测试

编写如下测试类

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;

//测试完成时间
@BenchmarkMode(Mode.AverageTime)
//设置统计结果的时间单位
@OutputTimeUnit(TimeUnit.NANOSECONDS)
//预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
//
//iterations:预热的次数
//time:每次预热的时间
//timeUnit:时间的单位,默认秒
//batchSize:批处理大小,每次操作调用几次方法
//因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,
//所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
@Warmup(iterations = 2,time = 1,timeUnit = TimeUnit.SECONDS)
//测试次数和时间,参数同上
@Measurement(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS)
//fork一个线程,进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
@Fork(1)
//通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,
//如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:
//Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
//Scope.Group:同一个线程在同一个 group 里共享实例
//Scope.Thread:默认的 State,每个测试线程分配一个实例
@State(Scope.Thread)
public class HashMapCycleTest {
    static Map<Integer,String> map = new HashMap(){{
        for(int i = 0;i<100;i++){
            put(i,"value"+i);
        }
    }};

    public static void main(String[] args) throws RunnerException {
        //启动基准测试
        Options options = new OptionsBuilder()
                .include(HashMapCycleTest.class.getSimpleName())//要导入的测试类
                .build();
        new Runner(options).run();//执行测试
    }

    @Benchmark
    public void EntrySetForEach(){
        //遍历
        Iterator<Map.Entry<Integer,String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry<Integer,String> entry = iterator.next();
            Integer key = entry.getKey();
            String value = entry.getValue();
        }
    }

    @Benchmark
    public  void KeySetForEach(){

        Iterator<Integer> iterator = map.keySet().iterator();
        while(iterator.hasNext()){
            Integer key = iterator.next();
            String value = map.get(key);
        }
    }

    @Benchmark
    public  void ForEachEntrySet(){

        for (Map.Entry<Integer,String> entry:map.entrySet()) {
            Integer key = entry.getKey();
            String value = entry.getValue();
        }
    }

    @Benchmark
    public  void ForEachKeySet(){

        for(Integer key:map.keySet()){
            Integer k = key;
            String value = map.get(key);
        }
    }

    @Benchmark
    public  void LambdaForEach(){

        map.forEach((key,value)->{
            Integer k = key;
            String v = value;
        });
    }

    @Benchmark
    public void StreamSingle(){
        map.entrySet().stream().forEach((entry)->{
            Integer k = entry.getKey();
            String v = entry.getValue();
        });
    }

}

运行测试类

所有被添加了 @Benchmark 注解的⽅法都会被测试,因为 parallelStream 为多线程版本性能⼀定是最好的,所以就不参与测试了

测试结果

//Benchmark                         Mode  Cnt    Score     Error  Units
//HashMapCycleTest.EntrySetForEach  avgt    5  543.154 ±  36.365  ns/op
//HashMapCycleTest.ForEachEntrySet  avgt    5  533.511 ±  42.878  ns/op
//HashMapCycleTest.ForEachKeySet    avgt    5  805.146 ± 118.210  ns/op
//HashMapCycleTest.KeySetForEach    avgt    5  785.286 ±  68.913  ns/op
//HashMapCycleTest.LambdaForEach    avgt    5  429.048 ±  47.000  ns/op
//HashMapCycleTest.StreamSingle     avgt    5  501.687 ±  25.401  ns/op

 

结论:

从以上结果可以看出, lambda表达式和两个entrySet的性能相近,并且执⾏速度最快,接下来是 stream ,

然后是两个 keySet 。如果从性能⽅⾯考虑,我们应该尽量使⽤ lambda 或者是 entrySet 来遍历 Map 集合。