遍历HashMap的方法有多种,比如通过获取map的keySet, entrySet, iterator之后,都可以实现遍历,然而如果在遍历过程中对map进行读取之外的操作则需要注意使用的遍历方式和操作方法。

public class MapIteratorTest {

    private static Map<Integer, String> map = new HashMap<Integer, String>();

    public static void main(String[] args) {
//init
for(int i = 0; i < 10; i++){
map.put(i, "value" + i);
} for(Map.Entry<Integer, String> entry : map.entrySet()){
Integer key = entry.getKey();
if(key % 2 == 0){
System.out.println("To delete key " + key);
map.remove(key);
System.out.println("The key " + + key + " was deleted");
}
} System.out.println("map size = " + map.size());
for(Map.Entry<Integer, String> entry : map.entrySet()){
System.out.println( entry.getKey() +" = " + entry.getValue());
}
}
}

上面代码的输出结果为

To delete key 0
The key 0 was deleted
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
at java.util.HashMap$EntryIterator.next(HashMap.java:834)
at java.util.HashMap$EntryIterator.next(HashMap.java:832)
at com.gpzuestc.collection.MapIteratorTest.main(MapIteratorTest.java:60)

通过上面的输出可以发现第一个偶数key元素已经被成功remove,异常的抛出位置是在迭代器遍历下一个元素的时候。

如果把上面高亮的遍历代码替换成keySet的方式,通过keySet的remove操作同样会在遍历下个元素时抛出异常,示例如下。

Set<Integer> keySet = map.keySet();
for(Integer key : keySet){
if(key % 2 == 0){
System.out.println("To delete key " + key);
keySet.remove(key);
System.out.println("The key " + + key + " was deleted");
}
}

 

To delete key 0
The key 0 was deleted
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
at java.util.HashMap$KeyIterator.next(HashMap.java:828)
at com.gpzuestc.collection.MapIteratorTest.main(MapIteratorTest.java:49)

如果要实现遍历过程中进行remove操作,上面两种方式都不能使用,而是需要通过显示获取keySet或entrySet的iterator来实现。

Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while(it.hasNext()){
Map.Entry<Integer, String> entry = it.next();
Integer key = entry.getKey();
if(key % 2 == 0){
System.out.println("To delete key " + key);
it.remove();
System.out.println("The key " + + key + " was deleted"); }
}
To delete key 0
The key 0 was deleted
To delete key 2
The key 2 was deleted
To delete key 4
The key 4 was deleted
To delete key 6
The key 6 was deleted
To delete key 8
The key 8 was deleted
map size = 5
1 = value1
3 = value3
5 = value5
7 = value7
9 = value9

分析原因

其实上面的三种遍历方式从根本上讲都是使用的迭代器,之所以出现不同的结果是由于remove操作的实现不同决定的。

首先前两种方法都在调用nextEntry方法的同一个地方抛出了异常

final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
...
...
}

这里modCount是表示map中的元素被修改了几次(在移除,新加元素时此值都会自增),而expectedModCount是表示期望的修改次数,在迭代器构造的时候这两个值是相等,如果在遍历过程中这两个值出现了不同步就会抛出ConcurrentModificationException异常。

1、HashMap的remove方法实现

public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}

2、HashMap.KeySet的remove方法实现

public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}

3、HashMap.HashIterator的remove方法实现

public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}

以上三种实现方式都通过调用HashMap.removeEntryForKey方法来实现删除key的操作。在removeEntryForKey方法内只要移除了key modCount就会执行一次自增操作,此时modCount就与expectedModCount不一致了,上面三种remove实现中,只有第三种iterator的remove方法在调用完removeEntryForKey方法后同步了expectedModCount值与modCount相同,所以在遍历下个元素调用nextEntry方法时,iterator方式不会抛异常。

final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev; while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
} return e;
}

发散

1、如果是遍历过程中增加或修改数据呢?
增加或修改数据只能通过Map的put方法实现,在遍历过程中修改数据可以,但如果增加新key就会在下次循环时抛异常,因为在添加新key时modCount也会自增。

2、有些集合类也有同样的遍历问题,如ArrayList,通过Iterator方式可正确遍历完成remove操作,直接调用list的remove方法就会抛异常。

//会抛ConcurrentModificationException异常
for(String str : list){
list.remove(str);
} //正确遍历移除方式
Iterator<String> it = list.iterator();
while(it.hasNext()){
it.next();
it.remove();
}

3、jdk为什么这样设计,只允许通过iterator进行remove操作?
HashMap和keySet的remove方法都可以通过传递key参数删除任意的元素,而iterator只能删除当前元素(current),一旦删除的元素是iterator对象中next所正在引用的,如果没有通过modCount、 expectedModCount的比较实现快速失败抛出异常,下次循环该元素将成为current指向,此时iterator就遍历了一个已移除的过期数据。

Java遍历HashMap并修改(remove)(转载)的更多相关文章

  1. Java遍历HashMap并修改(remove)

    遍历HashMap的方法有多种,比如通过获取map的keySet, entrySet, iterator之后,都可以实现遍历,然而如果在遍历过程中对map进行读取之外的操作则需要注意使用的遍历方式和操 ...

  2. java遍历Hashmap/Hashtable的几种方法

    一>java遍历Hashtabe: import java.util.Hashtable; import java.util.Set; public class HashTableTest { ...

  3. java遍历hashMap、hashSet、Hashtable

    一.遍历HashMap Map<Integer, String> map = new HashMap<Integer, String>(); 方法一:效率高 for(Entry ...

  4. [Java] 遍历HashMap和HashMap转换成List的两种方式

    遍历HashMap和HashMap转换成List   /** * convert the map to the list(1) */ public static void main(String[] ...

  5. JAVA遍历HashMap和ArrayList

    List Map 基础信息 HashMap 最近写程序经常需要遍历集合,所以总结一下内容: 一.简单实现 Map map = new HashMap(); for(Object o : map.key ...

  6. java遍历HashMap的高效方法

    https://stackoverflow.com/questions/46898/how-do-i-efficiently-iterate-over-each-entry-in-a-java-map

  7. [转载] Java 遍历 Map 的 5 种方式

    目录 1 通过 keySet() 或 values() 方法遍历 2 通过 keySet 的 get(key) 获取值 3 通过 entrySet 遍历 4 通过迭代器 Iterator 遍历 5 通 ...

  8. Java中HashMap遍历的两种方式

    Java中HashMap遍历的两种方式 转]Java中HashMap遍历的两种方式原文地址: http://www.javaweb.cc/language/java/032291.shtml 第一种: ...

  9. java 中 HashMap 遍历与删除

    HashMap的遍历 方法一.这是最常见的并且在大多数情况下也是最可取的遍历方式 /** * 在键值都需要时使用 */ Map<Integer, Integer> map = new Ha ...

随机推荐

  1. https 建立连接过程

    http://blog.csdn.net/wangjun5159/article/details/51510594 思考问题的顺序 学技术时,总是会问什么?这里也不例外,https为什么会存在,它有什 ...

  2. elementUI el-input focus

    Q1. el-input 获取焦点 Q2. dialog中的 el-input获取焦点 Q3. dialog中有table table中有 el-input 要获取焦点 一个宗旨: this.$ref ...

  3. CentOS 7 安装telnet服务

    今天测试zabbix需要用到telnet服务,查询到Centos7下下载安装telnet服务的方法,特此整理记录! 一.通过yum下载安装telnet yum -y install xinetd te ...

  4. Python3爬虫 利用百度地图api得到城市经纬度

    有2种方式,第一种是利用urllib , 方法1:利用urllib , 先把url 转成urlcode,然后读取网页,读到网页再用json读取内容,比较麻烦. 可以在浏览器输入,看一下格式. http ...

  5. centos7下安装docker(15.3跨主机网络-macvlan)

    除了ovrlay,docker还开发了另一个支持跨主机容器的driver:macvlan macvlan本身是linu kernel模块,其功能是允许在同一物理网卡上配置多了MAC地址,即:多个int ...

  6. Python:Day29 信号量、条件变量

    信号量:semaphore 信号量是用来控制线程并发数的.(理解:虽然GIL任意时刻都只有一个线程被执行,但是所有线程都有资格去抢,semaphore就是用来控制抢的GIL的数量,只有获取了semap ...

  7. Floyed

    1.定义 可解任意两点间的最短路径 可判有向图或负权的最短路径问题,也可用于计算有向图的传递闭包 2.算法描述 简单点说,就是暴力遍历 时间复杂度O(n^3) 下面是简简单单的代码: #include ...

  8. 20175330 2018-2019-2 《Java程序设计》第八周学习总结

    # **教材学习内容总结### 本周学习<Java程序设计>第十五章:*** 泛型: 泛型(Generics)的主要目的是可以建立具有类型安全的集合框架,如链表.散列映射等数据结构.泛型类 ...

  9. 【转】强化学习(一)Deep Q-Network

    原文地址:https://www.hhyz.me/2018/08/05/2018-08-05-RL/ 1. 前言 虽然将深度学习和增强学习结合的想法在几年前就有人尝试,但真正成功的开端就是DeepMi ...

  10. JavaEE学习之Spring Security3.x——模拟数据库实现用户,权限,资源的管理

    一.引言 因项目需要最近研究了下Spring Security3.x,并模拟数据库实现用户,权限,资源的管理. 二.准备 1.了解一些Spring MVC相关知识: 2.了解一些AOP相关知识: 3. ...