HashMap 7大遍历方式+性能分析+安全分析

1 篇文章 0 订阅
1 篇文章 0 订阅

在这里插入图片描述

HashMap 7大遍历方式+性能分析+安全分析


随着Java 1.8 Streams API的发布,使得HashMap拥有更多的遍历方式,那么应该选择哪种方式便成了一个问题?


会从遍历方式 → 性能测试 → 安全测试 三种来分析各自的优缺点

在这里插入图片描述

有下面几种遍历方式:


  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 个方法的测试结果如下:
在这里插入图片描述


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

执行结果

在这里插入图片描述

测试结果:For 循环中删除数据非安全。


3、Lambda方式

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

从上面的代码可以看出,可以先使用 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());
    }
});

执行结果

在这里插入图片描述

测试结果: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 循环前删除数据在遍历也是线程安全的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值