背景
昨天小枫接到了一个公司的面试电话,其中一道面试题觉得有点意思,在这里和大家一起分享下,面试题是ArrayList如何洗掉指定元素,乍听很简单的问题,但是如果没有实际踩过坑很容易掉进面试官的陷阱中,我们一起来分析下吧,
问题分析
疑惑满满
小枫听到这个面试题的时候,心想这是什么水面试官,怎么问这幺简单的题目,心想一个for回圈加上equal判断再洗掉不就完事了吗?但是转念一想,不对,这里面肯定有陷阱,不然不会问这幺看似简单的问题,小枫突然想起来之前写代码的时候好像遇到过这个问题,也是在ArrayList中洗掉指定元素,但是直接for回圈remove元素的时候还抛出了例外,面试官的陷阱估计在这里,小枫暗自窃喜,找到了面试官埋下的陷阱,
小枫回想起当天的的测验情况,代码进行了脱敏改造,当初是要在ArrayList中洗掉指定元素,小枫三下五除二,酣畅淋漓的写下了如下的代码,信心满满的点了Run代码的按钮,结果尴尬了,抛例外了,
public class TestListMain {
public static void main(String[] args) {
List<String> result = new ArrayList<>();
result.add("a");
result.add("b");
result.add("c");
result.add("d");
for (String s : result) {
if ("b".equals(s)) {
result.remove("b");
}
}
}
}
一个大大红色的例外马上就出来了,OMG,怎么会这样呢,感觉代码没什么问题啊,赶紧看看抛了什么例外,在哪里抛的例外吧,可以看出来抛了一个ConcurrentModificationException的例外,而且是在Itr这个类中的一个检测方法中抛出来的例外,这是怎么回事呢?我们的原始代码中并没有这个Itr代码,真是百思不得其解,
拨云见日
既然从源代码分析不出来,我们就看下源代码编译后的class档案中的内容是怎样的吧,毕竟class档案才是JVM真正执行的代码,不看不知道,一看吓一跳,JDK原来是这幺玩的,原来如此,我们原始代码中的for-each陈述句,编译后的实际是以迭代器来代替执行的,
public class TestListMain {
public TestListMain() {
}
public static void main(String[] args) {
List<String> result = new ArrayList();
result.add("a");
result.add("b");
result.add("c");
result.add("d");
//创建迭代器
Iterator var2 = result.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if ("b".equals(s)) {
result.remove("b");
}
}
}
}
通过ArrayList创建的Itr这个内部类迭代器,于是for-each回圈就转化成了迭代器加while回圈的方式,原来看上去的for-each回圈被挂羊头卖狗肉了,
public Iterator<E> iterator() {
return new Itr();
}
Itr这个内部类迭代器,通过判断hasNext()来判断迭代器是否有内容,而next()方法则获取迭代器中的内容,
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
...
}
大致的程序如下所示:
真正抛例外的地方是这个检测方法, 当modCount与expectedModCount不相等的时候直接抛出例外了,那我们要看下modCount以及expectedModCount分别是什么,这里的modCount代表ArrayList的修改次数,而expectedModCount代表的是迭代器的修改次数,在创建Itr迭代器的时候,将modCount赋值给了expectedModCount,因此在本例中一开始modCount和expectedModCount都是4(添加了四次String元素),但是在获取到b元素之后,ArrayList进行了remove操作,因此modCount就累加为5了,因此在进行检查的时候就出现了不一致,最终导致了例外的产生,到此我们找到了抛例外的原因,回圈使用迭代器进行回圈,但是操作元素却是使用的ArrayList操作,因此迭代器在回圈的时候发现元素被修改了所以抛出例外,
我们再来思考下,为什么要有这个检测呢?这个例外到底起到什么作用呢?我们先来开下ConcurrentModificationException的注释是怎么描述的,简单理解就是不允许一个执行绪在修改集合,另一个执行绪在集合基础之上进行迭代,一旦检测到了这种情况就会通过fast-fail机制,抛出例外,防止后面的不可知状况,
/**
***
* For example, it is not generally permissible for one thread to modify a Collection
* while another thread is iterating over it. In general, the results of the
* iteration are undefined under these circumstances. Some Iterator
* implementations (including those of all the general purpose collection implementations
* provided by the JRE) may choose to throw this exception if this behavior is
* detected. Iterators that do this are known as <i>fail-fast</i> iterators,
* as they fail quickly and cleanly, rather that risking arbitrary,
* non-deterministic behavior at an undetermined time in the future.
***
**/
public class ConcurrentModificationException extends RuntimeException {
...
}
回顾整个程序
如何正确的洗掉
既然抛例外的原因是回圈使用了迭代器,而洗掉使用ArrayList导致检测不通过,那么我们就回圈使用迭代器,洗掉也是用迭代器,这样就可以保证一致了,
public class TestListMain {
public static void main(String[] args) {
List<String> result = new ArrayList<>();
result.add("a");
result.add("b");
result.add("c");
result.add("d");
Iterator<String> iterator = list.iterator();
while (iterator .hasNext()) {
String str = iterator.next();
if ("b".equals(str)) {
iterator.remove();
}
}
}
总结
本文主要对于ArrayList在for回圈中进行元素洗掉出现的例外进行原始码分析,这也是面试的时候经常出现的面试陷阱题,面试官通过这样看似简单的题目考察候选者的JDK原始码的掌握程度,
大家好,我是慕枫,感谢各位小伙伴点赞、收藏和评论,文章持续更新,我们下期再见!
微信搜索:慕枫技术笔记,优质文章持续更新,我们有学习打卡的群可以拉你进,一起努力冲击大厂,另外有很多学习以及面试的材料提供给大家,最近在派送年终福利,赶快来看看吧,
真正的大师永远怀着一颗学徒的心
0 评论