HashMap 에서 7 가지 옮 겨 다 니 는 방식 의 성능 분석

22106 단어 HashMap성능 분석
머리말
JDK 1.8 Streams API 가 발표 되면 서 HashMap 은 더 많은 스 트 리밍 방식 을 가지 게 되 었 으 나 그런 스 트 리밍 방식 을 선택해 야 합 니까?오히려 문제 가 되 었 다.
본 논문 의 주요 내용 은 다음 그림 과 같다.

2.HashMap 옮 겨 다 니 기
HashMap 은 큰 방향 에서 볼 때 다음 과 같은 4 가지 로 나 눌 수 있 습 니 다.
  • 교체 기(Iterator)방식 으로 옮 겨 다 니 기;
  • 각 방식 으로 옮 겨 다 니 기;
  • Lambda 표현 식 옮 겨 다 니 기(JDK 1.8+);
  • Streams API 옮 겨 다 니 기(JDK 1.8+).
  • 그러나 각 유형 에 따라 실현 방식 이 다 르 기 때문에 구체 적 인 옮 겨 다 니 는 방식 은 다음 과 같은 7 가지 로 나 눌 수 있다.
  • 교체 기(Iterator)Entry Set 방식 으로 옮 겨 다 니 기;
  • 교체 기(Iterator)키 세트 를 사용 하여 옮 겨 다 니 기;
  • For Each EntrySet 방식 으로 옮 겨 다 니 기;
  • For Each KeySet 방식 으로 옮 겨 다 니 기;
  • Lambda 표현 식 으로 옮 겨 다 니 기;
  • Streams API 단일 스 레 드 방식 으로 옮 겨 다 니 기;
  • Streams API 다 중 스 레 드 방식 으로 옮 겨 다 닙 니 다.
  • 다음은 모든 스 트 리밍 방식 의 구체 적 인 실현 코드 를 살 펴 보 겠 습 니 다.
    2.1,교체 기 EntrySet
    
    @Test
        public void testIterator() {
            //       HashMap
            Map<Integer, String> map = new HashMap();
            map.put(1, "Java");
            map.put(2, "JDK");
            map.put(3, "Spring Framework");
            map.put(4, "MyBatis framework");
            map.put(5, "Oracle Database");
            //   
            Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<Integer, String> entry = iterator.next();
                System.out.println(entry.getKey() + ":" + entry.getValue());
            }
        }
    실행 결과:

    2.2,교체 기 키 세트
    
    @Test
        public void testKeySet() {
            //       HashMap
            Map<Integer, String> map = new HashMap();
            map.put(1, "Java");
            map.put(2, "JDK");
            map.put(3, "Spring Framework");
            map.put(4, "MyBatis framework");
            map.put(5, "Test KeySet");
            //   
            Iterator<Integer> iterator = map.keySet().iterator();
            while (iterator.hasNext()) {
                Integer key = iterator.next();
                System.out.println(key + ":" + map.get(key));
            }
        }
    실행 결과:

    2.3、ForEachEntrySet
    
    @Test
        public void testForEachEntrySet() {
            //       HashMap
            Map<Integer, String> map = new HashMap();
            map.put(1, "Java");
            map.put(2, "JDK");
            map.put(3, "Spring Framework");
            map.put(4, "MyBatis framework");
            map.put(5, "Test ForEach EntrySet");
            //   
            for (Map.Entry<Integer, String> entry : map.entrySet()) {
                System.out.println(entry.getKey() + ":" + entry.getValue());
            }
        }
    실행 결과:

    2.4、ForEach KeySet
    
    @Test
        public void testForEachKeySet() {
            //       HashMap
            Map<Integer, String> map = new HashMap();
            map.put(1, "Java");
            map.put(2, "JDK");
            map.put(3, "Spring Framework");
            map.put(4, "MyBatis framework");
            map.put(5, "Test ForEach KeySet");
            //   
            for (Integer key : map.keySet()) {
                System.out.println(key + ":" + map.get(key));
            }
        }
    실행 결과:

    2.5、Lambda
    
    @Test
        public void testLambda() {
            //       HashMap
            Map<Integer, String> map = new HashMap();
            map.put(1, "Java");
            map.put(2, "JDK");
            map.put(3, "Spring Framework");
            map.put(4, "MyBatis framework");
            map.put(5, "Test Lambda");
            //   
            map.forEach((key, value) -> {
                System.out.println(key + ":" + value);
            });
        }
    실행 결과:

    2.6,Streams API 단일 스 레 드
    
    @Test
        public void testStreamApi() {
            //       HashMap
            Map<Integer, String> map = new HashMap();
            map.put(1, "Java");
            map.put(2, "JDK");
            map.put(3, "Spring Framework");
            map.put(4, "MyBatis framework");
            map.put(5, "Test Stream API");
            //   
            map.entrySet().stream().forEach((entry) -> {
                System.out.println(entry.getKey() + ":" + entry.getValue());
            });
        }
    실행 결과:

    2.7,Streams API 다 중 스 레 드
    
    @Test
        public void testParallelStreamApi() {
            //       HashMap
            Map<Integer, String> map = new HashMap();
            map.put(1, "Java");
            map.put(2, "JDK");
            map.put(3, "Spring Framework");
            map.put(4, "MyBatis framework");
            map.put(5, "Test Parallel Stream API");
            //   
            map.entrySet().parallelStream().forEach((entry) -> {
                System.out.println(entry.getKey() + ":" + entry.getValue());
            });
        }
    실행 결과:

    성능 분석
    다음은 Oracle 이 공식 적 으로 제공 하 는 성능 테스트 도구 JMH(Java Microbenchmark Harness,JAVA 마이크로 기준 테스트 세트)를 이용 하여 7 가지 순환 성능 을 테스트 해 보 겠 습 니 다.
    우선 JMH 프레임 워 크 를 도입 해 야 합 니 다.이번 구축 은 의존 사용 도 구 를 Gradle 로 하고 다음 과 같은 설정 을 도입 합 니 다.
    implementation "org.openjdk.jmh:jmh-core:1.23"
    implementation "org.openjdk.jmh:jmh-generator-annprocess:1.23"
    Maven 을 사용 하면 다음 설정 을 도입 할 수 있 습 니 다.
    
    <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-core -->
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.23</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.openjdk.jmh/jmh-generator-annprocess -->
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.23</version>
        <scope>provided</scope>
    </dependency>
    성능 테스트 코드 작성:
    
    //@BenchmarkMode(Mode.Throughput) //     :   
    @BenchmarkMode(Mode.AverageTime) //     :      
    //@OutputTimeUnit(TimeUnit.MILLISECONDS)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @Warmup(iterations = 4, time = 1, timeUnit = TimeUnit.SECONDS) //    4  ,   1s
    @Measurement(iterations = 10, time = 3, timeUnit = TimeUnit.SECONDS) //    10  ,   3s
    @Fork(1) // fork 1    
    @State(Scope.Thread) //           
    public class HashMapTest {
    
        static Map<Integer, String> map = new HashMap() {
            {
                for(int var1 = 0; var1 < 2; ++var1) {
                    this.put(var1, "Kevin:" + var1);
                }
    
            }
        };
    
        public static void main(String[] args) throws RunnerException {
            //       
            Options opt = new OptionsBuilder()
                    .include(HashMapTest.class.getSimpleName()) //        
                    .output("E:/IDEAWorkSpaces/Test/src/main/java/com/kevin/performance/jmh-map2.log") //          
                    .build();
            new Runner(opt).run(); //     
        }
    
        /**
         * Iterator   entrySet
         */
        @Benchmark
        public void entrySet() {
            //   
            Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<Integer, String> entry = iterator.next();
                Integer k = entry.getKey();
                String v = entry.getValue();
            }
        }
    
        /**
         * Foreach   entrySet
         */
        @Benchmark
        public void forEachEntrySet() {
            //   
            for (Map.Entry<Integer, String> entry : map.entrySet()) {
                Integer k = entry.getKey();
                String v = entry.getValue();
            }
        }
    
        /**
         * Iterator   keySet
         */
        @Benchmark
        public void keySet() {
            Iterator<Integer> iterator = map.keySet().iterator();
            while (iterator.hasNext()) {
                Integer k = iterator.next();
                String v = map.get(k);
            }
        }
    
        /**
         * Foreach   keySet
         */
        @Benchmark
        public void forEachKeySet() {
            for (Integer key : map.keySet()) {
                Integer k = key;
                String v = map.get(k);
            }
        }
    
        /**
         * Lambda  
         */
        @Benchmark
        public void lambda() {
            map.forEach((key, value) -> {
                Integer k = key;
                String v = value;
            });
        }
    
        /**
         *      
         */
        @Benchmark
        public void streamApi() {
            map.entrySet().stream().forEach((entry) -> {
                Integer k = entry.getKey();
                String v = entry.getValue();
            });
        }
    
        /**
         *      
         */
        public void parallelStreamApi() {
            map.entrySet().parallelStream().forEach((entry) -> {
                Integer k = entry.getKey();
                String v = entry.getValue();
            });
        }
    }
    @Benchmark주석 이 추 가 된 모든 방법 이 테스트 됩 니 다.(parallelStream 은 다 중 스 레 드 버 전 으로 성능 이 다른 단일 스 레 드 로 인해 이번 테스트 에 참여 하지 않 습 니 다)테스트 결 과 는 다음 과 같 습 니 다.

    그 중에서 Units 는 N/op 으로 실행 완료 시간(단 위 는 나 초)을 의미 하고 Score 는 평균 실행 시간 으로 분류 되 며±기 호 는 오 차 를 나타 낸다.이 같은 결 과 를 통 해 알 수 있 듯 이 두 개entrySet의 성능 이 비슷 하고 집행 속도 가 가장 빠르다.이 어stream두 개keySet,성능 이 가장 나 쁜 것 은KeySet이다.
    결론:
    이상 의 결 과 를 통 해 알 수 있 듯 이entrySet의 성능 은keySet의 성능 보다 배가 높 기 때문에 우 리 는 가능 한 한entrySet을 사용 하여 맵 집합 을 옮 겨 다 녀 야 한다.
    바이트 코드 분석
    이상 의 테스트 결 과 를 이해 하려 면 모든 스 트 리밍 코드javac를 바이트 코드 로 컴 파일 하여 구체 적 인 원인 을 볼 필요 가 있다.
    컴 파일 후,우 리 는 Idea 를 사용 하여 바이트 코드 를 엽 니 다.내용 은 다음 과 같 습 니 다.
    
    public class HashMapTest {
        static Map<Integer, String> map = new HashMap() {
            {
                for(int var1 = 0; var1 < 2; ++var1) {
                    this.put(var1, "Kevin:" + var1);
                }
            }
        };
    
        public HashMapTest() {
        }
    
        public static void main(String[] var0) {
            entrySet();
            keySet();
            forEachEntrySet();
            forEachKeySet();
            lambda();
            streamApi();
            parallelStreamApi();
        }
    
        public static void entrySet() {
            Iterator var0 = map.entrySet().iterator();
    
            while(var0.hasNext()) {
                Entry var1 = (Entry)var0.next();
                System.out.println(var1.getKey() + ":" + (String)var1.getValue());
            }
        }
    
        public static void keySet() {
            Iterator var0 = map.keySet().iterator();
    
            while(var0.hasNext()) {
                Integer var1 = (Integer)var0.next();
                System.out.println(var1 + ":" + (String)map.get(var1));
            }
        }
    
        public static void forEachEntrySet() {
            Iterator var0 = map.entrySet().iterator();
    
            while(var0.hasNext()) {
                Entry var1 = (Entry)var0.next();
                System.out.println(var1.getKey() + ":" + (String)var1.getValue());
            }
        }
    
        public static void forEachKeySet() {
            Iterator var0 = map.keySet().iterator();
    
            while(var0.hasNext()) {
                Integer var1 = (Integer)var0.next();
                System.out.println(var1 + ":" + (String)map.get(var1));            
            }
        }
    
        public static void lambda() {
            map.forEach((var0, var1) -> {
                System.out.println(var0 + ":" + var1);            
            });
        }
    
        public static void streamApi() {
            map.entrySet().stream().forEach((var0) -> {
                System.out.println(var0.getKey() + ":" + (String)var0.getValue());
            });
        }
    
        public static void parallelStreamApi() {
            map.entrySet().parallelStream().forEach((var0) -> {
                System.out.println(var0.getKey() + ":" + (String)var0.getValue());
            });
        }
    }
    
    //       ,   Lambda   Streams API   ,         for        EntrySet            ,                  Entry ,    :
    
    public static void entrySet() {
        Iterator var0 = map.entrySet().iterator();
        while(var0.hasNext()) {
            Entry var1 = (Entry)var0.next();
            System.out.println(var1.getKey() + ":" + (String)var1.getValue());        
        }
    }
    public static void forEachEntrySet() {
        Iterator var0 = map.entrySet().iterator();
        while(var0.hasNext()) {
            Entry var1 = (Entry)var0.next();
            System.out.println(var1.getKey() + ":" + (String)var1.getValue());
        }
    }
    //  KeySet         ,    :
    
    public static void keySet() {
        Iterator var0 = map.keySet().iterator();
        while(var0.hasNext()) {
            Integer var1 = (Integer)var0.next();
            System.out.println(var1 + ":" + (String)map.get(var1));
        }
    } 
    public static void forEachKeySet() {
        Iterator var0 = map.keySet().iterator();
        while(var0.hasNext()) {
            Integer var1 = (Integer)var0.next();
            System.out.println(var1 + ":" + (String)map.get(var1));
        }
    }
    결 과 를 통 해 알 수 있 듯 이 Lambda 와 Streams API 를 제외 하고 교체 기 순환 과for순환 을 통 해 옮 겨 다 니 는EntrySet최종 적 으로 생 성 된 코드 는 똑 같 습 니 다.그들 은 모두 순환 에서 옮 겨 다 니 는 대상Entry을 만 들 었 습 니 다.코드 는 다음 과 같 습 니 다.
    
    public static void entrySet() {
        Iterator var0 = map.entrySet().iterator();
        while(var0.hasNext()) {
            Entry var1 = (Entry)var0.next();
            System.out.println(var1.getKey() + ":" + (String)var1.getValue());
        }
    }
    public static void forEachEntrySet() {
        Iterator var0 = map.entrySet().iterator();
        while(var0.hasNext()) {
            Entry var1 = (Entry)var0.next();
            System.out.println(var1.getKey() + ":" + (String)var1.getValue());
        }
    }
    그리고KeySet의 코드 도 비슷 하 다.다음 과 같다.
    
    public static void keySet() {
        Iterator var0 = map.keySet().iterator();
        while(var0.hasNext()) {
            Integer var1 = (Integer)var0.next();
            System.out.println(var1 + ":" + (String)map.get(var1));
        }
    } 
    public static void forEachKeySet() {
        Iterator var0 = map.keySet().iterator();
        while(var0.hasNext()) {
            Integer var1 = (Integer)var0.next();
            System.out.println(var1 + ":" + (String)map.get(var1));
        }
    }
    그래서 우리 가 교체 기 를 사용 하거나for순환EntrySet을 사용 할 때 그들의 성능 은 모두 같다.왜냐하면 그들 이 최종 적 으로 생 성 한 바이트 코드 는 기본적으로 똑 같 기 때문이다.같은 이치KeySet의 두 가지 옮 겨 다 니 는 방식 도 비슷 하 다.
    5.EntrySet 성능 분석EntrySetKeySet보다 성능 이 높 은 이 유 는KeySet순환 할 때map.get(key)를 사 용 했 고map.get(key)지도 집합 을 다시 한 번 옮 겨 다 니 며 조회key에 대응 하 는 값 을 조회 한 셈 이기 때문이다.왜'또'라 는 단 어 를 써 야 합 니까?교체 기 나 for 순환 을 사용 할 때 맵 집합 을 한 번 옮 겨 다 녔 기 때문에 다시map.get(key)조 회 를 사용 할 때 두 번 옮 겨 다 니 는 셈 이다.
    그리고EntrySet맵 집합 을 한 번 만 옮 겨 다 녔 고 그 후에 코드'Entryentry=iterator.next()'를 통 해 대상 의keyvalue값 을 모두Entry대상 에 넣 었 기 때문에keyvalue값 을 얻 을 때 맵 집합 을 옮 겨 다 니 지 않 고Entry대상 에서 값 을 얻 으 면 된다.
    그래서EntrySet의 성능 은KeySet의 성능 보다 배가 높 았 다.KeySet는 맵 집합 을 두 번 순환 한 셈 이 고EntrySet는 한 번 만 순환 한 셈 이기 때문이다.
    6.안전성 테스트
    위의 성능 테스트 결과 와 원리 분석 을 통 해 저 는 여러분 들 이 그런 옮 겨 다 니 는 방식 을 선택해 야 한다 고 생각 합 니 다.그 다음 에 우 리 는'안전'의 측면 에서 출발 하여 그런 옮 겨 다 니 는 방식 을 분석 하 는 것 이 더욱 안전 합 니 다.
    우 리 는 상기 사 이 를 네 가지 로 나 누 어 테스트 를 진행 합 니 다.교체 기 방식,For 순환 방식,Lambda 방식 과 Stream 방식,테스트 코드 는 다음 과 같 습 니 다.
    6.1 교체 기 방식
    
    Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<Integer, String> entry = iterator.next();
        if (entry.getKey() == 1) {
            //   
            System.out.println("del:" + entry.getKey());
            iterator.remove();
        } else {
            System.out.println("show:" + entry.getKey());
        }
    }
    실행 결과:
    show:0
    del:1
    show:2
    테스트 결과:교체 기 에서 데 이 터 를 순환 적 으로 삭제 하 는 것 이 안전 합 니 다.
    6.2 For 순환 방식
    
    for (Map.Entry<Integer, String> entry : map.entrySet()) {
        if (entry.getKey() == 1) {
            //   
            System.out.println("del:" + entry.getKey());
            map.remove(entry.getKey());
        } else {
            System.out.println("show:" + entry.getKey());
        }
    }
    실행 결과:

    테스트 결과:For 순환 에서 데 이 터 를 삭제 하 는 것 은 안전 하지 않 습 니 다.
    6.3 람 다 방식
    
    map.forEach((key, value) -> {
        if (key == 1) {
            System.out.println("del:" + key);
            map.remove(key);
        } else {
            System.out.println("show:" + key);
        }
    });
    실행 결과:

    테스트 결과:Lambda 순환 에서 데 이 터 를 삭제 하 는 것 은 안전 하지 않 습 니 다.
    Lambda 에서 삭제 하 는 올 바른 방법:
    
    //    map    key      
    map.keySet().removeIf(key -> key == 1);
    map.forEach((key, value) -> {
        System.out.println("show:" + key);
    });
    실행 결과:
    show:0
    show:2
    위의 코드 를 보면 알 수 있 듯 이LambdaremoveIf를 사용 하여 불필요 한 데 이 터 를 삭제 한 다음 에 순환 을 하 는 것 은 집합 을 정확하게 조작 하 는 방식 이다.
    6.4、스 트림 방식
    
    map.entrySet().stream().forEach((entry) -> {
        if (entry.getKey() == 1) {
            System.out.println("del:" + entry.getKey());
            map.remove(entry.getKey());
        } else {
            System.out.println("show:" + entry.getKey());
        }
    });
    실행 결과:

    테스트 결과:Stream 순환 에서 데 이 터 를 삭제 하 는 것 은 안전 하지 않 습 니 다.
    Stream 순환 의 올 바른 방식:
    
    map.entrySet().stream().filter(m -> 1 != m.getKey()).forEach((entry) -> {
        if (entry.getKey() == 1) {
            System.out.println("del:" + entry.getKey());
        } else {
            System.out.println("show:" + entry.getKey());
        }
    });
    실행 결과:
    show:0
    show:2
    위의 코드 를 통 해 알 수 있 듯 이Stream중의filter을 사용 하여 쓸모없는 데 이 터 를 걸 러 내 고 옮 겨 다 니 는 것 도 안전 한 조작 집합 방식 이다.
    6.5 소결
    우 리 는 사 이 를 옮 겨 다 니 며 집합map.remove()을 사용 하여 데 이 터 를 삭제 할 수 없다.이것 은 비 안전 한 조작 방식 이지 만,우 리 는 교체 기iterator.remove()의 방법 으로 데 이 터 를 삭제 할 수 있다.이것 은 안전하게 집합 을 삭제 하 는 방식 이다.마찬가지 로 저 희 는 Lambda 의removeIf를 사용 하여 데 이 터 를 미리 삭제 하거나 Stream 의filter을 사용 하여 삭제 할 데 이 터 를 걸 러 내 고 순환 할 수 있 습 니 다.이것 은 모두 안전 합 니 다.물론 저 희 는for순환 전에 데 이 터 를 삭제 할 수 있 습 니 다.
    7.총화
    본 고 는 HashMap 4 가지 옮 겨 다 니 는 방식 을 설명 했다.교체 기,for,lambda,stream,그리고 구체 적 인 7 가지 옮 겨 다 니 는 방법,종합 적 인 성능 과 안전성 을 보면 우 리 는 가능 한 한 교체 기(Iterator)를 사용 하여 옮 겨 다 니 는EntrySet방식 으로 Map 집합 을 조작 해 야 한다.그러면 안전 하면 서도 효율 적 이다.
    이상 은 HashMap 에서 7 가지 옮 겨 다 니 는 방식 의 성능 분석 에 대한 상세 한 내용 입 니 다.HashMap 옮 겨 다 니 는 성능 분석 에 관 한 자 료 는 다른 관련 글 을 주목 하 세 요!

    좋은 웹페이지 즐겨찾기