Java面试-容器的遍历
当我们用增强for循环遍历非并发容器(HashMap、ArrayList等),如果修改其结构,会抛出异常ConcurrentModificationException
,因此在阿里巴巴的Java规范中有说到:不要在foreach循环里进行元素的remove/add操作,remove元素请使用Iterator方式。
,但是不是真的就不可以在增强for循环中修改结构吗?其原理又是什么呢?
ConcurrentModificationException的含义
ConcurrentModificationException
可以将其通俗的翻译为并发修改异常
,那么关注点就在并发
和修改
了。也许有些人会说,我只是在单线程中修改了,并没有并发操作,但系统也抛了这样的这样的错误,这是为什么呢?别急,我们看看它的源码解释:
This exception may be thrown by methods that have detected concurrent modification of an object when such modification is not permissible.
这个异常就是应用程序在做一些系统不允许的操作时抛出的。记住,只要是系统不允许的操作,就一定会抛错的。
后面有一个值得注意的地方
Note that fail-fast behavior cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast operations throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: ConcurrentModificationException should be used only to detect bugs.
fail-fast
(快速失败)并不能一定被保证,所以fail-fast
操作会尽最大努力抛出该异常。既然是尽最大努力,因此无论是不是并发操作,只要是修改了,就一定会报错。
既然如此,我们来看看for循环中遍历修改容器结构,系统是如何知道的。
增加for循环的原理
我们来看看增强for循环遍历修改HashMap的代码:
Map<String, String> hashMap = new HashMap<>(10);
// 添加
for (int i = 0; i < 10; i++) {
hashMap.put("key" + i, "value" + i);
}
// 遍历修改
for (Entry<String, String> entry : hashMap.entrySet()) {
String key = entry.getKey();
hashMap.remove(key);
}
这个时候,你如果运行的话,就会抛出ConcurrentModificationException
,这个时候我们需要具体调试一下,发现遍历第一次并删除时没有报错,但第二次遍历,在for循环的括号执行完后,就抛出了异常,这又是为什么呢?
让我们反编译一下class文件,看看究竟增强for循环做了什么:
Map<String, String> hashMap = new HashMap(10);
for(int i = 0; i < 10; ++i) {
hashMap.put("key" + i, "value" + i);
}
Iterator var5 = hashMap.entrySet().iterator();
while(var5.hasNext()) {
Entry<String, String> entry = (Entry)var5.next();
String key = (String)entry.getKey();
hashMap.remove(key);
}
我们发现,虽然写法上是增强for循环,但实际还是使用的while
结合iterator
进行遍历,现在我们贴上这个代码进行调试。
发现在第二次var5.next()
处抛异常,接下来我们看看next
方法究竟做了什么?
在HashMap
的源码中显示:
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
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;
}
我们注意到,nextNode()
方法的第一个判断就决定了是否抛出ConcurrentModificationException
,那么modCount
和expectedModCount
究竟是什么呢?
modCount和expectedModCount
我们来看看modCount
和expectedModCount
的关系,当我们调用Iterator var5 = hashMap.entrySet().iterator();
时,源代码做了什么:
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
在一开始,就让expectedModCount
等于modCount
,而当我们调用hashMap.remove(key);
时,实际上修改了modCount
的值:
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 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
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;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
modCount
增大1,那么,当我们下一次调用var5.next()
时,自然就发现modCount
和expectedModCount
不等了。
修改结构的正确姿势
使用增强for循环
,本质还是在使用iterator
,那为什么大家都在推介使用iterator.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;
}
我们发现,这个remove方法虽然也调用了removeNode
,但它在最后一步再次将modCount
的值赋给expectedModCount
,因此保证了下一次调用next()
方法是不抛错。
所以,我们要么就直接显示地使用iterator
,用它的remove
方法移除对象。如果你实在想用增强for循环
遍历删除,那么也只能在删除一个后,立刻退出循环。但无论用哪种方法,当多个线程同时修改时,都会有出错的可能性,因为你即时保证单个线程内的modCount
和expectedModCount
,但这个操作并不能保证原子性。
总结
如果在多线程环境下,我更推介使用ConcurrentHashMap
,因为它没有modCount
和expectedModCount
的概念,因此,即时你是使用增强for循环
遍历删除,也不会出现问题。
有兴趣的话可以关注我的公众号或者头条号,说不定会有意外的惊喜。
Java面试-容器的遍历的更多相关文章
- Java面试容器,collection,list,set
1.容器指的是可以容纳其他对象的对象. 2.collection/set/list的联系和区别? (1)collection是Java集合顶级接口,存储一组不唯一,无序的对象: (2)list接口 ...
- 转:最近5年133个Java面试问题列表
最近5年133个Java面试问题列表 Java 面试随着时间的改变而改变.在过去的日子里,当你知道 String 和 StringBuilder 的区别就能让你直接进入第二轮面试,但是现在问题变得越来 ...
- Java面试葵花宝典
面向对象的特征有哪些方面 1. 抽象:抽象就是忽略一个主题中与当前目标2. 无关的那些方面,3. 以便更充分地注意与当前目标4. 有关的方面.抽象并不5. 打算了解全部问题,而6. 只是选择其中的一 ...
- java面试和笔试大全 分类: 面试 2015-07-10 22:07 10人阅读 评论(0) 收藏
2.String是最基本的数据类型吗? 基本数据类型包括byte.int.char.long.float.double.boolean和short. java.lang.String类是final类型 ...
- Java面试宝典2013版(超长版)
一. Java基础部分......................................................................................... ...
- 近5年133个Java面试问题列表
Java 面试随着时间的改变而改变.在过去的日子里,当你知道 String 和 StringBuilder 的区别就能让你直接进入第二轮面试,但是现在问题变得越来越高级,面试官问的问题也更深入. 在我 ...
- java 面试
115个Java面试题和答案——终极列表(上) 本文我们将要讨论Java面试中的各种不同类型的面试题,它们可以让雇主测试应聘者的Java和通用的面向对象编程的能力.下面的章节分为上下两篇,第一 ...
- Java面试宝典2014版
一. Java基础部分......................................................................................... ...
- java面试笔试大汇总
java面试笔试题大汇总5 JAVA相关基础知识 1.面向对象的特征有哪些方面 1.抽象:2.继承:3.封装:4. 多态性: 2.String是最基本的数据类型吗? 基本数据类型包括byte.int. ...
随机推荐
- Docker部署网站之后映射域名
Docker中部署tomcat相信大家也都知道,不知道的可以google 或者bing 一下.这里主要是为了记录在我们启动容器之后,tomcat需要直接定位到网站信息,而不是打开域名之后,还得加个bl ...
- Selenium+java - PageFactory设计模式
前言 上一小节我们已经学习了Page Object设计模式,优势很明显,能更好的体现java的面向对象思想和封装特性.但同时也存在一些不足之处,那就是随着这种模式使用,随着元素定位获取,元素定位与页面 ...
- Hyper-V修改Docker for Windows存储设置
自从Win10安装了Docker for Windows后,C盘容量爆炸,必须做一波迁移了.我的Docker使用的是Windows的Hyper-V虚拟机,于是google了一番找到以下一篇文章 doc ...
- MySQL数据库的安装和配置
MySQL数据库介绍 什么是数据库DB? DB的全称是database,即数据库的意思.数据库实际上就是一个文件集合,是一个存储数据的仓库,数据库是按照特定的格式把数据存储起来,用户可以对存储的数据进 ...
- aabccd统计每个字符出现的次数,结果显示{ a: 2, b: 1, c: 2, d: 1 };去掉重复的字符,使结果显示abcd
遍历字符串的方式和遍历数组的方式有点相似,或者说就是相同的.在学习数组的遍历方法之前,可以通过for循环去遍历数组,同样,字符串也可以:字符串跟数组都有一个length的属性.下面代码奉上,个人思路! ...
- springboot的异步调用
package com.handsight.platform.fras.aapp; import java.util.Locale; import org.slf4j.Logger; import o ...
- eclipse导入的web项目不能部署到tomcat,显示为java项目
今天在eclipse中导入之前做个项目,想运行起来看看,发现导入之后没法部署. 先解决办法如下: 右键项目 勾选上面三项并选择相应的值后就变成web项目,可以部署在tomcat上了.
- JavaScript清除空格、换行,把双引号转换成单引号
1.页面 2.源码 <!DOCTYPE> <html> <head> <meta charset="utf-8"> <titl ...
- MSIL实用指南-Action的生成和调用
MSIL实用指南-Action的生成和调用 System.Action用于封装一个没有参数没有返回值的方法.这里生成需要Ldftn指令. 下面讲解怎生成如下的程序. class ActionTest ...
- HDU-2795Billboard+对宽度建立线段树
参考: https://blog.csdn.net/qiqi_skystar/article/details/49073309 传送门:http://acm.hdu.edu.cn/showprobl ...