Java HashMap 如何正确遍历并删除元素

HashMap遍历与删除
本文介绍了HashMap的两种遍历方法及删除元素时的注意事项,特别针对键值为HashMap的情况提供了解决方案。

转载自:http://www.cnblogs.com/zhangnf/p/HashMap.html

(一)HashMap的遍历

 

  HashMap的遍历主要有两种方式:

  第一种采用的是foreach模式,适用于不需要修改HashMap内元素的遍历,只需要获取元素的键/值的情况。

复制代码
HashMap<K, V> myHashMap;
for (Map.Entry<K, V> item : myHashMap.entrySet()){
    K key = item.getKey();
    V val = item.getValue();
    //todo with key and val
    //WARNING: DO NOT CHANGE key AND val IF YOU WANT TO REMOVE ITEMS LATER
}
复制代码

 

  第二种采用迭代器遍历,不仅适用于HashMap,对其它类型的容器同样适用。

  采用这种方法的遍历,可以用下文提及的方式安全地对HashMap内的元素进行修改,并不会对后续的删除操作造成影响。

复制代码
for (Iterator<Map.entry<K, V>> it = myHashMap.entrySet().iterator; it.hasNext();){
    Map.Entry<K, V> item = it.next();
    K key = item.getKey();
    V val = item.getValue();
    //todo with key and val
    //you may remove this item using  "it.remove();"
}
复制代码

 

 

(二)HashMap之删除元素

 

  如果采用第一种的遍历方法删除HashMap中的元素,Java很有可能会在运行时抛出异常。

复制代码
HashMap<String, Integer> myHashMap = new HashMap<>();
myHashMap.put("1", 1);
myHashMap.put("2", 2);
for (Map.Entry<String, Integer> item : myHashMap.entrySet()){
    myHashMap.remove(item.getKey());
}
for (Map.Entry<String, Integer> item : myHashMap.entrySet()){
    System.out.println(item.getKey());
}
复制代码

  运行上面的代码,Java抛出了 java.util.ConcurrentModificationException 的异常。并附有如下信息。

at java.util.HashMap$HashIterator.nextNode(Unknown Source)
at java.util.HashMap$EntryIterator.next(Unknown Source)
at java.util.HashMap$EntryIterator.next(Unknown Source)

  可以推测,由于我们在遍历HashMap的元素过程中删除了当前所在元素,下一个待访问的元素的指针也由此丢失了。


  所以,我们改用第二种遍历方式。

  代码如下:

复制代码
for (Iterator<Map.Entry<String, Integer>> it = myHashMap.entrySet().iterator(); it.hasNext();){
    Map.Entry<String, Integer> item = it.next();
    //... todo with item
    it.remove();
}
for (Map.Entry<String, Integer> item : myHashMap.entrySet()){
    System.out.println(item.getKey());
}
复制代码

  运行结果没有显示,表明HashMap中的元素被正确删除了。


 

(三)在HashMap的遍历中删除元素的特殊情况

 

  上述方法可能足以应付多数的情况,但是如果你的HashMap中的键值同样是一个HashMap,假设你需要处理的是 HashMap<HashMap<String, Integer>, Double> myHashMap 时,很不碰巧,你可能需要修改myHashMap中的一个项的键值HashMap中的某些元素,之后再将其删除。

  这时,单单依靠迭代器的 remove() 方法是不足以将该元素删除的。

  例子如下:

复制代码
HashMap<HashMap<String, Integer>, Integer> myHashMap = new HashMap<>();
HashMap<String, Integer> temp = new HashMap<>();
temp.put("1", 1);
temp.put("2", 2);
myHashMap.put(temp, 3);
for (Iterator<Map.Entry<HashMap<String, Integer>, Integer>> 
    it = myHashMap.entrySet().iterator(); it.hasNext();){
    Map.Entry<HashMap<String, Integer>, Integer> item = it.next();
    item.getKey().remove("1");
    System.out.println(myHashMap.size());
    it.remove();
    System.out.println(myHashMap.size());
}
复制代码

  结果如下:

1
1

  虽然 it.remove(); 被执行,但是并没有真正删除元素。

  原因在于期望删除的元素的键值(即 HashMap<String, Integer> temp )被修改过了。


  解决方案:

  既然在这种情况下,HashMap中被修改过的元素不能被删除,那么不妨直接把待修改的元素直接删除,再将原本所需要的“修改过”的元素加入HashMap。

  想法很好,代码如下:

复制代码
for (Iterator<Map.Entry<HashMap<String, Integer>, Integer>> 
    it = myHashMap.entrySet().iterator(); it.hasNext();){
    Map.Entry<HashMap<String, Integer>, Integer> item = it.next();
    //item.getKey().remove("1");
    HashMap<String, Integer> to_put = new HashMap<>(item.getKey());
    to_put.remove("1");
    myHashMap.put(to_put, item.getValue());
    System.out.println(myHashMap.size());
    it.remove();
    System.out.println(myHashMap.size());
}
复制代码

  但是依然是RE:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.remove(Unknown Source)

  原因在于,迭代器遍历时,每一次调用 next() 函数,至多只能对容器修改一次。上面的代码则进行了两次修改:一次添加,一次删除。


  既然 java.util.ConcurrentModificationException 异常被抛出了,那么去想办法拿掉这个异常即可。

  最后的最后,我决定弃HashMap转投ConcurrentHashMap。将myHashMap定义为ConcurrentHashMap之后,其它代码不动。

  运行结果如下:

2
1

  最终,通过ConcurrentHashMap和一些小技巧的使用,变形实现了对被修改过键值的元素的删除。



<think>好的,我现在要解决用户的问题:如何在遍历Java HashMap时安全地删除或添加数据。用户还提到了最佳实践,所以我需要详细解释正确的方法以及背后的原理。 首先,我需要回忆一下HashMap的基本结构和遍历时的行为。HashMap是线程安全的,所以在遍历时修改结构(添加或删除元素)可能会导致ConcurrentModificationException异常。这是因为HashMap内部有一个modCount变量,记录结构修改的次数。当使用迭代器遍历时,迭代器会记录创建时的modCount值,每次访问下一个元素时会检查这个值是否变化,如果变化了就会抛出异常。 接下来,用户的问题具体是在遍历时如何安全地删除或添加数据。常见的遍历方式有几种,比如使用keySet()、entrySet()、或者迭代器。同的遍历方式在处理修改时的行为可能同。 首先,传统的for-each循环遍历时,如果直接调用remove()方法,比如在遍历keySet的时候用HashMap的remove方法,就会触发ConcurrentModificationException。例如: ```java HashMap<String, Integer> map = new HashMap<>(); // 添加元素 for (String key : map.keySet()) { if (条件) { map.remove(key); // 这里会抛出异常 } } ``` 这是因为for-each循环背后使用的是迭代器,而直接调用map的remove方法会更新迭代器的状态,导致modCount和迭代器中的expectedModCount一致,从而抛出异常。 正确的做法是使用迭代器自身的remove方法。例如: ```java Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); if (条件) { iterator.remove(); // 安全删除 } } ``` 这里通过迭代器的remove方法,会同步更新modCount和expectedModCount,避免异常。同样的,如果在遍历过程中需要添加元素,普通的迭代器是支持添加操作的,这时候可能需要考虑其他方法,比如在遍历时记录需要添加的元素遍历结束后再添加,或者使用发集合类如ConcurrentHashMap。 另外,Java 8引入了removeIf方法,可以更方便地删除符合条件的元素: ```java map.entrySet().removeIf(entry -> 条件); ``` 这种方法内部也是使用迭代器的remove,因此也是安全的。 关于添加元素,常规的遍历过程中无法直接添加,因为这会改变结构,导致迭代器失效。可能的解决方案包括: 1. 收集需要添加的键值对,遍历结束后统一添加。 2. 使用发集合类,如ConcurrentHashMap,它允许在遍历时进行修改,但需要注意线程安全的问题。 此外,用户可能需要注意在遍历过程中修改元素的值(而非结构)是否安全。例如,使用entry.setValue()方法修改值通常会导致结构变化,因此是允许的,会抛出异常。但需要确保这种修改会影响其他部分代码的正确性。 总结最佳实践: - 删除元素时使用迭代器的remove方法或removeIf。 - 添加元素时,避免在遍历过程中直接添加,而是先收集再批量添加,或使用线程安全的集合。 - 避免在遍历时使用HashMap自身的修改方法,而是通过迭代器操作。 需要验证这些信息是否正确,可以查阅Java官方文档或可靠的源码分析资料。例如,HashMap的迭代器实现中,remove方法确实会调整expectedModCount,而直接调用map的remove会。同时,ConcurrentHashMap的设计允许更安全的发修改,但可能涉及更复杂的线程管理。 最后,用户提供的引用中有提到HashMap的结构修改可能导致问题,比如源码中的modCount机制,以及扩容条件等。这支持了上述关于ConcurrentModificationException的解释。例如引用[5]提到在链表长度到8时,可能会扩容而是树化,这说明结构修改确实会影响HashMap的内部状态,进而影响遍历时的行为。</think>### Java HashMap遍历时安全删除/添加数据的实践指南 #### 一、遍历删除数据的安全方法 1. **迭代器直接操作** 使用`Iterator.remove()`方法删除当前元素,这是唯一在遍历过程中**安全删除**的方式: ```java Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); if (entry.getKey().equals("banana")) { iterator.remove(); // 通过迭代器删除,避免ConcurrentModificationException[^5] } } ``` 2. **Java 8+的removeIf方法** 通过集合的批量删除方法实现更简洁的操作: ```java map.entrySet().removeIf(entry -> entry.getValue() > 2); // 删除值大于2的条目[^2] ``` #### 二、遍历时添加数据的处理策略 由于HashMap遍历过程**允许结构性修改**(添加/删除元素),需采用以下方法: 1. **延迟添加模式** ```java List<Map.Entry<String, Integer>> toAdd = new ArrayList<>(); for (Map.Entry<String, Integer> entry : map.entrySet()) { if (entry.getValue() == 1) { toAdd.add(new AbstractMap.SimpleEntry<>("grape", 4)); } } toAdd.forEach(e -> map.put(e.getKey(), e.getValue())); // 遍历结束后统一添加[^3] ``` 2. **使用发容器** 改用`ConcurrentHashMap`支持遍历时修改(需注意线程安全): ```java ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(map); concurrentMap.forEach((k, v) -> { if (v == 2) { concurrentMap.put("melon", 5); // 允许遍历时添加(特定线程安全场景下) } }); ``` #### 三、关键注意事项 | 操作类型 | 安全方式 | 危险方式 | |----------|-----------------------------|----------------------------| | 删除元素 | `iterator.remove()`/`removeIf` | `map.remove(key)`直接调用 | | 修改值 | `entry.setValue()` | 无风险但可能影响业务逻辑 | | 添加元素 | 遍历后批量添加/使用发容器 | 遍历中直接`map.put()`触发异常 | #### 四、异常触发原理 HashMap通过`modCount`计数器跟踪结构性修改,迭代器创建时会记录初始值`expectedModCount`。当发现`modCount != expectedModCount`时,立即抛出`ConcurrentModificationException`。 #### 五、最佳实践总结 1. **删除优先用迭代器** 避免直接操作集合的删除方法 2. **添加采用缓冲策略** 先记录待添加数据遍历结束后处理 3. **高发场景选发容器** 使用`ConcurrentHashMap`或`Collections.synchronizedMap()` 4. **值修改无需特殊处理** `entry.setValue()`会触发结构变更
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值