天天看點

HashMap 7大周遊方式+性能分析+安全分析 HashMap 7大周遊方式+性能分析+安全分析

HashMap 7大周遊方式+性能分析+安全分析 HashMap 7大周遊方式+性能分析+安全分析

HashMap 7大周遊方式+性能分析+安全分析

随着Java 1.8 Streams API的釋出,使得HashMap擁有更多的周遊方式,那麼應該選擇哪種方式便成了一個問題?

會從周遊方式 → 性能測試 → 安全測試 三種來分析各自的優缺點

HashMap 7大周遊方式+性能分析+安全分析 HashMap 7大周遊方式+性能分析+安全分析

有下面幾種周遊方式:

  1. 使用疊代器(Iterator)EntrySet 的方式進行周遊;
  2. 使用疊代器(Iterator)KeySet 的方式進行周遊;
  3. 使用 For Each EntrySet 的方式進行周遊;
  4. 使用 For Each KeySet 的方式進行周遊;
  5. 使用 Lambda 表達式的方式進行周遊;
  6. 使用 Streams API 單線程的方式進行周遊;
  7. 使用 Streams API 多線程的方式進行周遊。
接下來我們看具體代碼實作

1、疊代器EntrySet

public static void main(String[] args) {
    Map<String, Object> map = new HashMap<>();
    map.put("1", "JavaSE");
    map.put("2", "Spring");
    map.put("3", "MaBatis");
    map.put("4", "Spring Boot");
    map.put("5", "JavaEE");
    /*********************疊代器EntrySet周遊Map*********************/
    //将Map中的鍵和值傳遞給疊代器
    Iterator<Map.Entry<String, Object>> iterator = map.entrySet().iterator();
    //判斷下一個是否有值
    while (iterator.hasNext()) {
        Map.Entry<String, Object> entry = iterator.next();
        System.out.print("鍵:" + entry.getKey() + "--------");
        System.out.println("值:" + entry.getValue());
    }
}
           
執行結果
鍵:1--------值:JavaSE
鍵:2--------值:Spring
鍵:3--------值:MaBatis
鍵:4--------值:Spring Boot
鍵:5--------值:JavaEE
           

2、疊代器KeySet

public static void main(String[] args) {
    Map<String, Object> map = new HashMap<>();
    map.put("1", "JavaSE");
    map.put("2", "Spring");
    map.put("3", "MaBatis");
    map.put("4", "Spring Boot");
    map.put("5", "JavaEE");
    /*********************疊代器KeySet周遊Map*********************/
    //将Map中的鍵傳遞給疊代器
    Iterator<String> iterator = map.keySet().iterator();
    while (iterator.hasNext()) {
    String key = iterator.next();
    System.out.print("鍵:" + key + "-------");
    System.out.println("值:" + map.get(key));
    }
 }
           
執行結果
鍵:1-------值:JavaSE
鍵:2-------值:Spring
鍵:3-------值:MaBatis
鍵:4-------值:Spring Boot
鍵:5-------值:JavaEE
           

3、ForEach EntrySet

public static void main(String[] args) {
    Map<String, Object> map = new HashMap<>();
    map.put("1", "JavaSE");
    map.put("2", "Spring");
    map.put("3", "MaBatis");
    map.put("4", "Spring Boot");
    map.put("5", "JavaEE");
    /*********************疊代器KeySet周遊Map*********************/
    for (Map.Entry<String,Object> entry : map.entrySet()){
    System.out.print("鍵:" + entry.getKey() + "-------");
    System.out.println("值:" + entry.getValue());
    }
 }
           
執行結果
鍵:1-------值:JavaSE
鍵:2-------值:Spring
鍵:3-------值:MaBatis
鍵:4-------值:Spring Boot
鍵:5-------值:JavaEE
           

4、ForEach KeySet

public static void main(String[] args) {
    Map<String, Object> map = new HashMap<>();
    map.put("1", "JavaSE");
    map.put("2", "Spring");
    map.put("3", "MaBatis");
    map.put("4", "Spring Boot");
    map.put("5", "JavaEE");
    /*********************疊代器KeySet周遊Map*********************/
    for (String key : map.keySet()){
    System.out.print("鍵:" + key + "-------");
    System.out.println("值:" + map.get(key));
    }
 }
           
執行結果
鍵:1-------值:JavaSE
鍵:2-------值:Spring
鍵:3-------值:MaBatis
鍵:4-------值:Spring Boot
鍵:5-------值:JavaEE
           

5、Lambda

public static void main(String[] args) {
    Map<String, Object> map = new HashMap<>();
    map.put("1", "JavaSE");
    map.put("2", "Spring");
    map.put("3", "MaBatis");
    map.put("4", "Spring Boot");
    map.put("5", "JavaEE");
    /*********************疊代器KeySet周遊Map*********************/
    map.forEach((key, value) -> {
    System.out.print("鍵:" + key + "------");
    System.out.println("值:" + value);
    });
 }
           
執行結果
鍵:1------值:JavaSE
鍵:2------值:Spring
鍵:3------值:MaBatis
鍵:4------值:Spring Boot
鍵:5------值:JavaEE
           

6、Streams API 單線程

public static void main(String[] args) {
    Map<String, Object> map = new HashMap<>();
    map.put("1", "JavaSE");
    map.put("2", "Spring");
    map.put("3", "MaBatis");
    map.put("4", "Spring Boot");
    map.put("5", "JavaEE");
    /*********************疊代器KeySet周遊Map*********************/
    map.entrySet().stream().forEach((entry) -> {
    System.out.print("鍵:" + entry.getKey() + "-------");
    System.out.println("值:" + entry.getValue());
    });
 }
           
執行結果
鍵:1-------值:JavaSE
鍵:2-------值:Spring
鍵:3-------值:MaBatis
鍵:4-------值:Spring Boot
鍵:5-------值:JavaEE
           

7、Streams API 多線程

public static void main(String[] args) {
    Map<String, Object> map = new HashMap<>();
    map.put("1", "JavaSE");
    map.put("2", "Spring");
    map.put("3", "MaBatis");
    map.put("4", "Spring Boot");
    map.put("5", "JavaEE");
    /*********************疊代器KeySet周遊Map*********************/
    map.entrySet().parallelStream().forEach((entry) -> {
    System.out.print("鍵:" + entry.getKey() + "--------");
    System.out.println("值:" + entry.getValue());
    });
 }
           
執行結果
鍵:4--------鍵:1--------值:JavaSE
值:Spring Boot
鍵:2--------鍵:5--------值:JavaEE
值:Spring
鍵:3--------值:MaBatis
           
注意:因為是多線程,不能保證資料是按順序輸出

性能測試

接下來我們使用 Oracle 官方提供的性能測試工具 JMH 來測試一下這 7 種循環的性能。

首先,先要引入 JMH 架構,在 pom.xml 檔案中添加如下配置:

<!-- 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.AverageTime) // 測試完成時間
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS) // 預熱 2 輪,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 測試 5 輪,每次 1s
@Fork(1) // fork 1 個線程
@State(Scope.Thread) // 每個測試線程一個執行個體
public class HashMapCycleTest {
    static Map<Integer, String> map = new HashMap() {{
        // 添加資料
        for (int i = 0; i < 100; i++) {
            put(i, "val:" + i);
        }
    }};

    public static void main(String[] args) throws RunnerException {
        // 啟動基準測試
        Options opt = new OptionsBuilder()
                .include(HashMapCycle.class.getSimpleName()) // 要導入的測試類
                .output("/Users/admin/Desktop/jmh-map.log") // 輸出測試結果的檔案
                .build();
        new Runner(opt).run(); // 執行測試
    }

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

    @Benchmark
    public void forEachEntrySet() {
        // 周遊
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            Integer k = entry.getKey();
            String v = entry.getValue();
        }
    }

    @Benchmark
    public void keySet() {
        // 周遊
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer k = iterator.next();
            String v = map.get(k);
        }
    }

    @Benchmark
    public void forEachKeySet() {
        // 周遊
        for (Integer key : map.keySet()) {
            Integer k = key;
            String v = map.get(k);
        }
    }

    @Benchmark
    public void lambda() {
        // 周遊
        map.forEach((key, value) -> {
            Integer k = key;
            String v = map.get(k);
        });
    }

    @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 為多線程版本性能一定是最好的,是以就不參與測試了,其他 6 個方法的測試結果如下:

HashMap 7大周遊方式+性能分析+安全分析 HashMap 7大周遊方式+性能分析+安全分析

其中 Score 清單示平均執行時間, ± 符号表示誤差。從以上結果可以看出,lambda 表達式和兩個 entrySet 的性能相近,并且執行速度最快,接下來是 stream ,然後是兩個 keySet。

結論

如果從性能方面考慮,我們應該盡量使用 lambda 或者是 entrySet 來周遊 Map 集合

性能分析

EntrySet 之是以比 KeySet 的性能高是因為,KeySet 在循環時使用了 map.get(key),而

map.get(key) 相當于又周遊了一遍 Map 集合去查詢 key 所對應的值。為什麼要用“又”這個詞?那是因為在使用疊代器或者

for 循環時,其實已經周遊了一遍 Map 集合了,是以再使用 map.get(key) 查詢時,相當于周遊了兩遍。 而 EntrySet

隻周遊了一遍 Map 集合,之後通過代碼“Entry<Integer, String> entry =

iterator.next()”把對象的 key 和 value 值都放入到了 Entry 對象中,是以再擷取 key 和 value

值時就無需再周遊 Map 集合,隻需要從 Entry 對象中取值就可以了。 是以,EntrySet 的性能比 KeySet

的性能高出了一倍,因為 KeySet 相當于循環了兩遍 Map 集合,而 EntrySet 隻循環了一遍。

安全性測試

我們把以上周遊劃分為四類進行測試:疊代器方式、For 循環方式、Lambda 方式和 Stream 方式,測試代碼如下。

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
           
測試結果:疊代器中循環删除資料安全。

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());
    }
}
           
執行結果
HashMap 7大周遊方式+性能分析+安全分析 HashMap 7大周遊方式+性能分析+安全分析
測試結果:For 循環中删除資料非安全。

3、Lambda方式

map.forEach((key, value) -> {
    if (key == 1) {
        System.out.println("del:" + key);
        map.remove(key);
    } else {
        System.out.println("show:" + key);
    }
});
           
執行結果
HashMap 7大周遊方式+性能分析+安全分析 HashMap 7大周遊方式+性能分析+安全分析
測試結果:Lambda 循環中删除資料非安全。

Lambda 删除的正确方式:

// 根據 map 中的 key 去判斷删除
map.keySet().removeIf(key -> key == 1);
map.forEach((key, value) -> {
    System.out.println("show:" + key);
});
           
執行結果:
show:0
show:2
           

從上面的代碼可以看出,可以先使用 Lambda 的 removeIf 删除多餘的資料,再進行循環是一種正确操作集合的方式。

4、Stream 方式

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());
    }
});
           
執行結果
HashMap 7大周遊方式+性能分析+安全分析 HashMap 7大周遊方式+性能分析+安全分析
測試結果: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 過濾掉無用的資料,再進行周遊也是一種安全的操作集合的方式。

結論

我們不能在周遊中使用集合 map.remove() 來删除資料,這是非安全的操作方式,但我們可以使用疊代器的

iterator.remove() 的方法來删除資料,這是安全的删除集合的方式。同樣的我們也可以使用 Lambda 中的

removeIf 來提前删除資料,或者是使用 Stream 中的 filter

過濾掉要删除的資料進行循環,這樣都是安全的,當然我們也可以在 for 循環前删除資料在周遊也是線程安全的。