增强for循环
很多初学者都有过这样的经历,在for循环遍历一个集合时,有时删除其中的某一个元素会报错,示例代码如下:
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {
{
add("a");
add("b");
add("c");
}
};
for (String s : list) {
if (s.equals("a")) {
list.remove(s);
}
}
}
}
输出:Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at com.hard.qz.kayakta.test.Test.main(Test.java:20)
根据报错提示,我们可以发现,我在调用ArrayList的内部类Itr的next()方法,next()方法调用了checkForComodification()方法报错了。但是我们并没有去调用next方法。通过查询Test类编译后的字节码文件发现,增强for循环在编译后,被优化为迭代器Iterator遍历的方式。
//上述源码编译后的字节码
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if (s.equals("a")) {
list.remove(s);
}
}
至此,我们找到了调用Itr类的next()方法的原因,但是为什么会报错呢?我们看下checkForComodification()方法体的内容
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
modCount变量是ArrayList从AbstractList类继承来的,它用来记录list被修改的次数,每次执行add()方法或remove方法时,都会对modCount+1。
expectedModCount变量是内部类Itr的成员变量,它的初始值等于modCount。
看到这里我恍然大悟了,原来这个方法是为了防止,在用Iterator迭代器遍历List时,有线程对List进行修改做的安全性校验。
但为什么有时候使用增强for循环删除元素不会报错呢?示例代码如下:
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {
{
add("a");
add("b");
add("c");
}
};
for (String s : list) {
if (s.equals("b")) {
list.remove(s);
}
}
System.out.println(list);
}
}
输出:[a, c]
我们来进入remove()方法中,看下元素到底是如何被删除的。
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
原来我们每调用remove删除一个元素,底层都会调用System.arraycopy方法拷贝元素。在上述代码中,删除前的元素在内存中的格式为**["a","b","c"],删除后的元素格式为["a","c",null]**,且数组的size被修改为2了。
从remove()方法中没有看什么端倪,此时我们将目光转向iterator的hasNext()方法。
public boolean hasNext() {
return cursor != size;
}
这里的size是数组的元素个数,cursor是什么呢?
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
...
}
从源码注释中,可以看出cursor是指向返回的下一个元素的索引。
在遍历到元素b时,cursor为2,删除b后,c被拷贝到原来b的位置,size减小为2,cursor依然是2,此时执行hasNext()方法,cursor == size,hasNex()方法返回了false,还没有执行next()方法遍历到元素c,程序就退出了while循环,所以删除b元素没有报错。至此我们找到了在增强for循环中删除倒数第二个元素不报错的原因---即因为执行remove后,size-1,正好等于cursor,所以最后一个元素就不会被遍历了。
总结:增强for循环在编译后,是Iterator迭代器遍历的形式,虽然有时删除特定位置上的元素不会报错,但是我们也不能在for增强循环中增删改元素。
普通for循环
普通for循环中删除元素的代码示例:
public class Test {
public Test() {
}
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {
{
this.add("a");
this.add("b");
this.add("c");
}
};
for(int i = 0; i < list.size(); ++i) {
if (((String)list.get(i)).equals("c")) {
list.remove(i);
}
}
System.out.println(list);
}
}
输出:[a, b]
通过和增强for循环对比,普通for循环编译后还是原来的模样,没有被优化为Iterator的形式。所以在普通for循环中删除或新增元素并不会报错。
但是当删除两个相邻且重复的元素时,会有漏删的问题,示例代码如下:
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {
{
add("a");
add("b");
add("b");
add("c");
}
};
for (int i = 0; i < list.size(); i++) {
if(list.get(i).equals("b")){
list.remove(i);
}
}
System.out.println(list);
}
}
输出:[a, b, c]
我们预期输出的结果应该为[a,c],为什么有一个b元素没有被删除呢?
再次点开remove()方法我们发现
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
当遍历到第一个b元素时,程序执行了remove操作,后面的b和c往前挪了一个位置,第二个b占了第一个b的位置,所以删除完之后,for循环的指针指向的是第二个b元素,接着执行i++,进入下次循环,访问到c元素,跳过了第二个b元素,所以导致了漏删。
总结:当不删除相邻重复的元素时,在for循环体中执行删除操作是没有问题的。但是我们仍然不建议在普通for循环中删除元素,我们推荐使用Interator迭代器去删除元素。如果非要使用普通for循环删除元素时,执行remove操作后,需要进行i--操作。
Iterator迭代器
Iterator是rt包下的一个接口,其下有两个抽象方法,两个有默认实现的方法。
public interface Iterator<E> {
//判断是否有元素
boolean hasNext();
//获取下一个元素
E next();
//删除元素,默认是不允许的
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
在ArrayList中有一个内部类实现了Iterator接口
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() {}
...
//Itr类中的remove
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>() {
{
add("a");
add("b");
add("b");
add("c");
}
};
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String next = iterator.next();
if(next == "b"){
iterator.remove();
}
}
System.out.println(list);
}
}
输出:[a, c]
可以看到,当我们使用Iterator删除任意元素时,都不会报错且结果符合我们预期。
最后我们回答下标题提出的问题,遍历集合的过程中可以删除元素吗?可以,但是必须使用Iterator迭代器删除元素。
Iterator抛出异常是快速失败(fail-fast)的体现。
什么是快速失败呢?
快速失败是指在遍历的过程中,如果当前线程发现容器中的元素被修改了,就直接抛出异常,这里抛出的是concurrentModificationException(并发修改异常),java.util包下的hashMap、ArrayList等集合都是采用fail-fast的方式。而ConcurrentHashMap、CopyOnWriteArrayList是采用fail-safe的方式,这些集合在遍历的过程中允许修改元素,但是它遍历的是原有集合的副本。