场景

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

Java中使用JMH(Java Microbenchmark Harness 微基准测试框架)进行性能测试和优化_java热点函数

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


实现

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());
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

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));
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

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());
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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));
        }
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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);
        });
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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());
        });
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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());
        });
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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();
        });
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.

运行测试类

所有被添加了 @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

Java性能优化-HashMap遍历的7种方式及性能测试对比_开发语言

结论:

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

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