为什么map不能遍历的同时进行增删操作?

本文探讨了在Java中使用foreach循环遍历HashMap时为何不能同时进行put、remove、add操作,分析了底层原理,并通过源码解释了ConcurrentModificationException异常的原因。建议在遍历集合时使用Iterator进行操作,以避免并发修改异常。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前段时间,同事在代码中KW扫描的时候出现这样一条:

上面出现这样的原因是在使用foreach对HashMap进行遍历时,同时进行put赋值操作会有问题,异常ConcurrentModificationException。

于是帮同简单的看了一下,印象中集合类在进行遍历时同时进行删除或者添加操作时需要谨慎,一般使用迭代器进行操作。

于是告诉同事,应该使用迭代器Iterator来对集合元素进行操作。同事问我为什么?这一下子把我问蒙了?对啊,只是记得这样用不可以,但是好像自己从来没有细究过为什么?

于是今天决定把这个HashMap遍历操作好好地研究一番,防止采坑!

foreach循环?

java foreach 语法是在jdk1.5时加入的新特性,主要是当作for语法的一个增强,那么它的底层到底是怎么实现的呢?下面我们来好好研究一下:

foreach 语法内部,对collection是用iterator迭代器来实现的,对数组是用下标遍历来实现。Java 5 及以上的编译器隐藏了基于iteration和数组下标遍历的内部实现。

(注意,这里说的是“Java编译器”或Java语言对其实现做了隐藏,而不是某段Java代码对其实现做了隐藏,也就是说,我们在任何一段JDK的Java代码中都找不到这里被隐藏的实现。这里的实现,隐藏在了Java 编译器中,查看一段foreach的Java代码编译成的字节码,从中揣测它到底是怎么实现的了)

我们写一个例子来研究一下:

public class HashMapIteratorDemo {
	
	String[] arr = {"aa", "bb", "cc"};
	
	public void test1() {
		for(String str : arr) {
		}
	}
}
复制代码

将上面的例子转为字节码反编译一下(主函数部分):

也许我们不能很清楚这些指令到底有什么作用,但是我们可以对比一下下面段代码产生的字节码指令:


public class HashMapIteratorDemo2 {
	
	String[] arr = {"aa", "bb", "cc"};
	
	public void test1() {
		for(int i = 0; i < arr.length; i++) {
			String str = arr[i];
		}
	} 
}
复制代码

看看两个字节码文件,有木有发现指令几乎相同,如果还有疑问我们再看看对集合的foreach操作:

通过foreach遍历集合:


public class HashMapIteratorDemo3 {
	
	List<Integer> list = new ArrayList<>();
	
	public void test1() {
		list.add(1);
		list.add(2);
		list.add(3);
		
		for(Integer var : list) {
		}
	}
}
复制代码

通过Iterator遍历集合:


public class HashMapIteratorDemo4 {
	
	List<Integer> list = new ArrayList<>();
	
	public void test1() {
		list.add(1);
		list.add(2);
		list.add(3);
		
		Iterator<Integer> it = list.iterator();
		while(it.hasNext()) {
			Integer var = it.next();
		}
	}
}
复制代码

将两个方法的字节码对比如下:

我们发现两个方法字节码指令操作几乎一模一样;

这样我们可以得出以下结论:

对集合来说,由于集合都实现了Iterator迭代器,foreach语法最终被编译器转为了对Iterator.next()的调用;

对于数组来说,就是转化为对数组中的每一个元素的循环引用。

HashMap遍历集合并对集合元素进行remove、put、add

1、现象

根据以上分析,我们知道HashMap底层是实现了Iterator迭代器的 ,那么理论上我们也是可以使用迭代器进行遍历的,这倒是不假,例如下面:


public class HashMapIteratorDemo5 {
	
	public static void main(String[] args) {
		Map<Integer, String> map = new HashMap<>();
		map.put(1, "aa");
		map.put(2, "bb");
		map.put(3, "cc");
		
		for(Map.Entry<Integer, String> entry : map.entrySet()){  
		    int k=entry.getKey();  
		    String v=entry.getValue();  
		    System.out.println(k+" = "+v);  
		}  
	} 
}
复制代码

输出:

ok,遍历没有问题,那么操作集合元素remove、put、add呢?


public class HashMapIteratorDemo5 {
	
	public static void main(String[] args) {
		Map<Integer, String> map = new HashMap<>();
		map.put(1, "aa");
		map.put(2, "bb");
		map.put(3, "cc");
		
		for(Map.Entry<Integer, String> entry : map.entrySet()){  
		    int k=entry.getKey();  
		    if(k == 1) {
		    	map.put(1, "AA");
		    }
		    String v=entry.getValue();  
		    System.out.println(k+" = "+v);  
		}  
	} 
}
复制代码

执行结果:

执行没有问题,put操作也成功了。

但是!但是!但是!问题来了!!!

我们知道HashMap是一个线程不安全的集合类,如果使用foreach遍历时,进行add,remove操作会java.util.ConcurrentModificationException异常。put操作可能会抛出该异常。(为什么说可能,这个我们后面解释)

为什么会抛出这个异常呢?

我们先去看一下java api文档对HasMap操作的解释吧。

翻译过来大致的意思就是该方法是返回此映射中包含的键的集合视图。集合由映射支持,如果在对集合进行迭代时修改了映射(通过迭代器自己的移除操作除外),则迭代的结果是未定义的。集合支持元素移除,通过Iterator.remove、set.remove、removeAll、retainal和clear操作从映射中移除相应的映射。简单说,就是通过map.entrySet()这种方式遍历集合时,不能对集合本身进行remove、add等操作,需要使用迭代器进行操作。

对于put操作,如果这个操作时替换操作如上例中将第一个元素进行修改,就没有抛出异常,但是如果是使用put添加元素的操作,则肯定会抛出异常了。我们把上面的例子修改一下:


public class HashMapIteratorDemo5 {
	
	public static void main(String[] args) {
		Map<Integer, String> map = new HashMap<>();
		map.put(1, "aa");
		map.put(2, "bb");
		map.put(3, "cc");
		
		for(Map.Entry<Integer, String> entry : map.entrySet()){  
		    int k=entry.getKey();  
		    if(k == 1) {
		    	map.put(4, "AA");
		    }
		    String v=entry.getValue();  
		    System.out.println(k+" = "+v);  
		}  
 
	} 
}
复制代码

执行出现异常:

这就是验证了上面说的put操作可能会抛出java.util.ConcurrentModificationException异常。

但是有疑问了,我们上面说过foreach循环就是通过迭代器进行的遍历啊?为什么到这里是不可以了呢?

这里其实很简单,原因是我们的遍历操作底层确实是通过迭代器进行的,但是我们的remove等操作是通过直接操作map进行的,如上例子:map.put(4, "AA");//这里实际还是直接对集合进行的操作,而不是通过迭代器进行操作。所以依然会存在ConcurrentModificationException异常问题。

2、细究底层原理

我们再去看看HashMap的源码,通过源代码,我们发现集合在使用Iterator进行遍历时都会用到这个方法:

final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
复制代码

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

现在我们来看看集合remove操作:

(1)HashMap本身的remove实现:

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
复制代码

(2)HashMap.KeySet的remove实现

public final boolean remove(Object key) {
    return removeNode(hash(key), key, null, false, true) != null;
}
复制代码

(3)HashMap.EntrySet的remove实现

public final boolean remove(Object o) {
    if (o instanceof Map.Entry) {
        Map.Entry<?,?> e = (Map.Entry<?,?>) o;
        Object key = e.getKey();
        Object value = e.getValue();
        return removeNode(hash(key), key, value, true, true) != null;
    }
    return false;
}
复制代码

(4)HashMap.HashIterator的remove方法实现

public final void remove() {
    Node<K,V> p = current;
    if (p == null)
        throw new IllegalStateException();
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    current = null;
    K key = p.key;
    removeNode(hash(key), key, null, false, false);
    expectedModCount = modCount; //----------------这里将expectedModCount 与modCount进行同步
}
复制代码

以上四种方式都通过调用HashMap.removeNode方法来实现删除key的操作。在removeNode方法内只要移除了key, modCount就会执行一次自增操作,此时modCount就与expectedModCount不一致了;

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        ...
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;   //------------------------这里对modCount进行了自增,可能会导致后面与expectedModCount不一致
            --size;
            afterNodeRemoval(node);
            return node;
        }
        }
        return null;
   }
复制代码

上面三种remove实现中,只有第三种iterator的remove方法在调用完removeNode方法后同步了expectedModCount值与modCount相同,所以在遍历下个元素调用nextNode方法时,iterator方式不会抛异常。

到这里是不是有一种恍然大明白的感觉呢!

所以,如果需要对集合遍历时进行元素操作需要借助Iterator迭代器进行,如下:

public class HashMapIteratorDemo5 {
	
	public static void main(String[] args) {
		Map<Integer, String> map = new HashMap<>();
		map.put(1, "aa");
		map.put(2, "bb");
		map.put(3, "cc");
		//		for(Map.Entry<Integer, String> entry : map.entrySet()){  //		    int k=entry.getKey();  //		    //		    if(k == 1) {//		    	map.put(1, "AA");//		    }//		    String v=entry.getValue();  //		    System.out.println(k+" = "+v);  //		}  
		
		Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
		while(it.hasNext()){
			Map.Entry<Integer, String> entry = it.next();
			int key=entry.getKey();  
	        if(key == 1){  
	            it.remove();        
	        }  
		}
	}
}
<think>我们正在讨论的是在遍历集合时是否允许进行增删操作。根据引用[2]中的示例,我们看到在遍历HashMap时尝试修改(put操作)是允许的,但增删操作呢?实际上,在Java集合框架中,大多数集合在遍历时进行结构性修改(添加或删除元素)会导致`ConcurrentModificationException`异常。但具体是否允许以及原因需要详细解释。 首先,结构性修改是指添加、删除元素,或者显式调整底层数组大小(如ArrayList的扩容)等操作,这些操作会改变集合的结构。在遍历过程中进行结构性修改,可能会导致不可预期的行为,因此Java集合框架设计了一种快速失败(fail-fast)机制来检测并发修改。 以下分情况讨论: 1. **迭代器遍历时的限制**: 当使用显式的迭代器(如通过`iterator()`方法获取的Iterator)遍历集合时,在迭代过程中如果直接通过集合的方法(而不是迭代器自身的方法)进行增删操作,则会抛出`ConcurrentModificationException`。 但是,如果使用迭代器自身的`remove()`方法删除元素,则是安全的,因为迭代器会同步修改期望的修改次数(modCount)。 示例(错误做法): ```java List<String> list = new ArrayList<>(); list.add("A"); list.add("B"); Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if ("B".equals(s)) { list.remove(s); // 这里会抛出ConcurrentModificationException } } ``` 正确做法: ```java // 使用迭代器的remove方法 Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if ("B".equals(s)) { it.remove(); // 安全 } } ``` 2. **增强for循环(for-each)的限制**: 增强for循环底层也是使用迭代器,所以在循环体内直接调用集合的增删方法同样会抛出异常。 3. **普通for循环(使用索引)**: 对于基于索引的集合(如ArrayList),可以使用普通for循环并通过索引删除元素,但需要小心索引的变化。例如,删除一个元素后,后面的元素会前移,因此索引需要调整。 但是,如果在循环中增加元素,可能会导致死循环或者数组越界等问题,所以一般也不推荐。 4. **特定集合的特殊情况**: 如`CopyOnWriteArrayList`(并发集合)在迭代时允许修改,因为它会创建底层数组的一个新副本。但是,这种集合有性能开销,适用于读多写少的场景。 5. **Map遍历**: 对于Map,如HashMap,在遍历时(无论是通过entrySet、keySet还是values)进行增删操作,使用集合的方法也会导致`ConcurrentModificationException`。但是,引用[2]中的示例是修改已存在键的值(put操作),这不会改变Map的结构(不会增加或删除映射关系),所以不会抛出异常。但是,如果进行删除(remove)或添加新的键值对,则会抛出异常。 原因: 集合内部维护一个`modCount`(修改次数)字段。当创建迭代器时,迭代器会记录当前的`modCount`值(`expectedModCount`)。在每次迭代时,会检查这两个值是否相等,如果不相等,则抛出`ConcurrentModificationException`。直接调用集合的增删方法会修改`modCount`,而迭代器自身的`remove()`方法会在删除后同步更新`expectedModCount`,所以不会抛出异常。 总结: - 在遍历集合时,禁止直接调用集合的增删操作(结构性修改),否则会抛出`ConcurrentModificationException`。 - 可以使用迭代器自身的删除方法(`remove`)来安全删除元素(但添加操作没有提供相应方法)。 - 如果需要遍历增删,可以考虑使用并发集合类(如`ConcurrentHashMap`、`CopyOnWriteArrayList`)或使用普通for循环并谨慎处理索引(仅适用于删除,且需注意后续索引调整)。 引用[2]中的示例代码之所以没有抛出异常,是因为它执行的是更新操作(对已有键1重新赋值),这并不算结构性修改。根据HashMap的文档,如果put操作只是覆盖了已有键的值,而不是增加新的键值对(即没有改变映射关系的数量),则不会导致结构性修改,因此不会增加`modCount`,也就不会触发异常。 但是,如果在遍历过程中添加新的键值对(增加映射关系)或删除键值对,就会导致结构性修改,从而触发异常。 因此,回答用户的问题:集合类型在遍历时禁止增删操作(结构性修改),原因是Java集合框架的快速失败机制(fail-fast)为了避免在并发修改时导致不可预期的行为,而抛出`ConcurrentModificationException`异常。 下面我们用一个表格总结常见集合遍历增删操作的支持情况: | 遍历方式 | 直接调用集合的增删方法 | 使用迭代器自身的remove方法 | 备注 | |---------------------|------------------------|----------------------------|------| | 显式Iterator遍历 | 抛出异常 | 允许 | 无add方法 | | 增强for循环 | 抛出异常 | 不可用(无显式迭代器) | 底层是迭代器 | | ArrayList普通for循环 | 删除元素后需调整索引 | 不可用 | 删除元素后,后续元素索引前移,需将索引减1 | | CopyOnWriteArrayList | 允许(但迭代器看到的是旧快照) | 无(迭代器不支持remove) | 迭代器创建时的数组快照,修改操作不影响当前迭代 | 注意:普通for循环删除元素后,需要将索引减1,否则会跳过下一个元素。例如: ```java List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C")); for (int i=0; i<list.size(); i++) { if ("B".equals(list.get(i))) { list.remove(i); i--; // 删除后索引回退 } } ``` 但是,这种方式在并发环境下仍然不安全,所以单线程下谨慎使用,多线程下应使用并发集合。 最后,我们通过一个示例说明在遍历HashMap时进行结构性修改(增删)会抛出异常: ```java Map<Integer, String> map = new HashMap<>(); map.put(1, "A"); map.put(2, "B"); for (Map.Entry<Integer, String> entry : map.entrySet()) { if (entry.getKey() == 1) { map.remove(1); // 结构性修改,抛出ConcurrentModificationException } } ``` 而使用迭代器的remove方法则安全(通过entrySet的迭代器): ```java Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry<Integer, String> entry = it.next(); if (entry.getKey() == 1) { it.remove(); // 安全 } } ``` 因此,在遍历集合时,如果需要删除元素,应优先使用迭代器的remove方法。 引用[2]中提到了遍历时修改值(非结构性修改)是允许的,但增删操作(结构性修改)则会导致异常。</think>### 遍历集合时增删操作的规则与原理 在Java集合框架中,**遍历过程中直接进行增删操作通常是被禁止的**,这会导致`ConcurrentModificationException`异常。具体规则如下: #### 一、禁止操作的情况及原因 | 操作类型 | 是否允许 | 原因说明 | |----------------|----------|--------------------------------------------------------------------------| | **删除元素** | ❌ 禁止 | 会改变集合结构,导致迭代器状态失效[^2] | | **添加元素** | ❌ 禁止 | 改变集合大小,破坏迭代器的遍历逻辑 | | **修改值** | ✅ 允许 | 不影响集合结构(如`Map.put()`更新值)[^2] | **根本原因**:Java集合使用**快速失败(fail-fast)机制**保护迭代过程: 1. 每个集合维护`modCount`(修改计数器) 2. 迭代器创建时记录当前`modCount` 3. 每次迭代检查`modCount`是否变化 4. 若检测到变化(增删操作导致),立即抛出`ConcurrentModificationException` #### 二、安全操作的正确方式 1. **使用迭代器的删除方法** ```java // ArrayList示例 List<String> list = new ArrayList<>(List.of("A", "B", "C")); Iterator<String> it = list.iterator(); while (it.hasNext()) { if ("B".equals(it.next())) { it.remove(); // ✅ 安全删除 } } ``` 2. **使用`removeIf()`方法(Java 8+)** ```java list.removeIf(s -> s.equals("B")); // ✅ 线程安全 ``` 3. **创建副本遍历** ```java new ArrayList<>(list).forEach(item -> { if (condition) list.remove(item); // ✅ 操作原集合 }); ``` #### 三、特殊集合的例外情况 1. **`ConcurrentHashMap`** 允许并发修改,使用分段锁保证线程安全: ```java Map<String, Integer> map = new ConcurrentHashMap<>(); map.forEach((k, v) -> { if (k.equals("test")) map.remove(k); // ✅ 允许 }); ``` 2. **`CopyOnWriteArrayList`** 遍历基于创建时的快照,允许修改: ```java List<String> cowList = new CopyOnWriteArrayList<>(); cowList.addAll(List.of("X", "Y", "Z")); for (String s : cowList) { // 遍历快照副本 if (s.equals("Y")) cowList.add("W"); // ✅ 允许 } ``` #### 四、底层原理图解 ```mermaid graph TD A[开始遍历] --> B[创建迭代器] B --> C[记录当前 modCount=5] C --> D[遍历元素] D --> E{执行增删操作?} E -->|是| F[modCount++ → 6] E -->|否| D F --> G[迭代器检查 modCount] G -->|5 ≠ 6| H[抛出 ConcurrentModificationException] ``` #### 五、性能影响对比 | 方法 | 时间复杂度 | 线程安全 | 适用场景 | |--------------------|------------|----------|------------------------| | 直接增删 | O(1) | ❌ | 非遍历场景 | | 迭代器`remove()` | O(n) | ✅ | 小规模集合 | | `removeIf()` | O(n) | ✅ | Java 8+ 推荐方案 | | `ConcurrentHashMap`| O(1)分段 | ✅ | 高并发场景[^2] | > ⚠️ **关键结论**: > 遍历增删元素会破坏集合的结构一致性,Java通过`ConcurrentModificationException`强制开发者使用线程安全的修改方式,这是**集合框架保护数据完整性的重要机制**[^2][^4]。 --- ### 示例解析 ```java List<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry")); // ❌ 危险操作:直接删除 for (String fruit : fruits) { if (fruit.startsWith("B")) { fruits.remove(fruit); // 抛出ConcurrentModificationException } } // ✅ 正确方式:迭代器删除 Iterator<String> it = fruits.iterator(); while (it.hasNext()) { if (it.next().startsWith("B")) { it.remove(); // 安全删除 } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值