一、使用 Iterator 遍历集合
1、简要介绍
Iterator 是一个接口,提供了一种遍历集合元素的方法,而不需要关心集合的底层实现。迭代器允许我们在遍历过程中安全地删除集合中的元素。Iterator 的源码:
public interface Iterator<E> {
// 判断是否还有下一个元素
boolean hasNext();
// 获取下一个元素
E next();
// 删除当前迭代器指向的元素
default void remove() {
throw new UnsupportedOperationException("remove");
}
// ...
}
在使用 next() 方法前需要调用 hasNext() 方法判断是否有下一个有效元素。
2、使用示例
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class Example {
public static void main(String[] args) {
Collection<String> col = new ArrayList<String>();
col.add("Hello");
col.add("World");
col.add("HelloWorld");
// 获取集合对应的迭代器
Iterator<String> itr = col.iterator();
while(itr.hasNext()) { // 判断是否有下一个元素
String str = itr.next(); // 获取当下一个元素
System.out.println(str);
}
}
}
运行结果:
Hello
World
HelloWorld
如果这里不使用 hasNext() 方法来判断是否又下一个元素,则可能导致迭代器对象内部的游标属性指向的位置超出范围,这样就会抛出 NoSuchElementException 异常。
3、迭代器机制的底层实现
这里为了解释这个底层实现,再给出一个继承和实现关系图:

在 Iterable 接口中,有这样一个公有抽象方法:
Iterator<T> iterator();
这个方法是用来返回一个迭代器对象的,这个抽象方法,最终会在某个实现类中被实现,这里以 ArrayList 作为例子,所以 ArrayList 类中会有 iterator() 方法的具体实现。
对于这个返回的迭代器对象的类型,在 ArrayList 类的实现中是 ArrayList 的一个内部类 Itr,Itr 实现了 Iterator 接口。
我们可以查看 ArrayList 的部分源码来验证上面的说法:
public Iterator<E> iterator() { // 重写了 Iterable 接口的 iterator() 方法
return new Itr();
}
private class Itr implements Iterator<E> { // 内部类,实现了 Iterator 接口
int cursor; // 指向下一个要返回的元素的游标
int lastRet = -1; // 上一个返回的元素的索引,如果没有上一个则为 -1
int expectedModCount = modCount; // 同步元素个数,用于删除元素
public boolean hasNext() {
return cursor != size;
}
public E next() {
// ...
}
public void remove() {
// ...
}
}
二、使用 for-each 增强型循环遍历集合
1、使用示例
import java.util.ArrayList;
import java.util.Collection;
public class Example {
public static void main(String[] args) {
Collection<String> list = new ArrayList<>();
list.add("Hello");
list.add("你好");
list.add("World");
list.add("世界");
// for-each 循环遍历集合
for(String str : list) {
System.out.println(str);
}
}
}
运行结果:
Hello
你好
World
世界
2、for-each 循环底层实现
对于 for-each 循环底层实际上是使用迭代器进行实现的。
Collection<String> list = new ArrayList<>();
list.add("Hello");
list.add("你好");
list.add("World");
list.add("世界");
// for-each 循环遍历集合
for(String str : list) {
System.out.println(str);
}
对上面这段代码进行编译,然后反编译,可以得到:
Collection<String> list = new ArrayList();
list.add("Hello");
list.add("你好");
list.add("World");
list.add("世界");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String str = (String)var2.next();
System.out.println(str);
}
可以看到,for-each 循环直接被替换成了迭代器的标准循环了,所以说 for-each 的底层确实是通过迭代器实现的。
3、for-each 循环中不能直接使用集合的 remove() 方法删除元素
Collection<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
for(String str : list) {
if("World".equals(str)) {
list.remove("World");
}
}
尝试执行上面的代码,在删除语句执行后的一次循环中,会抛出一个 ConcurrentModificationException 异常。
下面我们将逐步分析原因:
首先,上上面的代码就相当于:
Collection<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
Iterator<String> itr = list.iterator();
while(itr.hasNext()) {
String str = itr.next();
if("World".equals(str)) {
list.remove("World");
}
}
显然,运行这段代码也会抛出异常。
当我们对上面的代码进行调试,我们会发现,异常是在 while 循环的第三次循环中的 String str = itr.next(); 语句抛出的异常,这是其中一个问题。明明只有两个元素,为什么会进入第三轮循环呢。
我们可以查看判断条件 hasNext() 方法的源码:
public boolean hasNext() {
return cursor != size;
}
这里的判断条件是 cursor != size; ,szie 是集合元素个数,也就是集合大小,cursor 是下一个要返回的元素的索引。
- 第一轮循环,首先
cursor的值为 0,这时size为 2,hasNext()判断为真,然后在next()方法调用后,0 位置的元素被返回,然后cursor变为 1,if语句条件不满足,所以不执行。 - 第二轮循环,
cursor的值为 1,这时size为 2,hasNext()判断为真,然后在next()方法调用后,1 位置的元素被返回,然后cursor变为 2,if语句条件满足,索引为 1 的元素"World"被删除,这时,size变成了 1。 - 由于上面的删除元素使
szie变成了 1,而cursor是 2,所以hasNext()判断还是为真,所以这才有了第三轮循环。
那第三轮循环中, cursor 大于了 size,不应当抛出 NoSuchElementException 异常吗,这里为什么抛出的是 ConcurrentModificationException 异常呢,这就要查看 next() 方法的源码了:
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];
}
上面的代码会首先执行 checkForComodification(); 方法,这个方法也是 Itr 内部类中的方法,这个方法的源码为:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
可以看到这个方法会判断两个值是否相等,如果不相等,则抛出我们上面提到的异常。那这两个值又是什么呢。
对于 modCount 属性,是 AbstractList 类的一个属性:
protected transient int modCount = 0;
我们知道 ArrayList 是 AbstractList 类的子类,这个属性又是 protected 的,所以可以直接在 ArrayList 中被访问,由于成员内部类可以访问外部类的所有属性,所以 Itr 类可以访问到这个 modCount 属性,这个属性的作用是记录这个集合被修改的次数,修改一次,则会导致这个属性自增一。
对于 expectedModCount 属性,是 Itr 类的一个属性,在 Itr 类被创建时,会被初始化为 modCount 的值:
int expectedModCount = modCount;
既然会抛出 ConcurrentModificationException 异常,就代表这两个属性在某个时候变为不相等了。
这时,我们就要查看 list.remove() 调用的这个 remove() 方法了,这个 remove() 方法是 AbstractList 实现的,在这个方法内部,又调用了 fastRemove() 方法,实际上真正的删除操作是由 fastRemove() 方法完成的,所以我们这里查看 fastRemove() 方法的源码:
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;
}
这里,我们可以看到 modCount 被修改了,而 expectedModCount 却没有变,所以这里导致两者不相等了。完成了这个删除操作后,进入第三轮循环时,会执行 next() 方法,next() 方法中又会首先调用 checkForComodification(); 方法,这时判断出两个属性不相等,就会抛出 ConcurrentModificationException 异常,即并发修改异常。
既然上面这种方法删除元素是错误的,那我们应当怎么删除元素呢。
4、推荐在迭代器循环中使用迭代器的 remove() 方法
想要使用迭代器的 remove() 方法,那就要显示声明一个迭代器,那就不能使用 for-each 循环了。
所以在循环中删除集合元素的标准做法为:
Collection<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
Iterator<String> itr = list.iterator();
while (itr.hasNext()) {
String str = itr.next();
if("World".equals(str)) {
itr.remove(); // 使用迭代器的 remove() 方法删除当前元素
}
}
这样既不会有第三次循环的问题,也不会有上面提到的两个属性不同导致抛出异常的问题。
怎么实现的呢,我们可以查看迭代器提供的 remove() 方法的源码:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet; // 关键,解决了 size 变化导致的循环次数超出的问题
lastRet = -1;
expectedModCount = modCount; // 关键,解决了两个属性不同导致的异常问题
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
虽然迭代器提供的 remove() 方法中中最终也会调用到 AbstractList 类的 fastRemove() 方法,会导致 modCount 自增,但是,迭代器中对一些属性进行了调整,解决了上面提到的两个问题。
对推荐代码的 while 循环进行分析:
- 第一轮循环,首先
cursor的值为 0,这时size为 2,hasNext()判断为真,然后在next()方法调用后,0 位置的元素被返回,然后cursor变为 1,if语句条件不满足,所以不执行。 - 第二轮循环,
cursor的值为 1,这时size为 2,hasNext()判断为真,然后在next()方法调用后,1 位置的元素被返回,然后cursor变为 2,if语句条件满足,执行删除,删除操作最终是由AbstractList类的fastRemove()方法完成,方法中对modCount自增一,然后对size自减一,这时,size变成了 1。
然后,迭代器中的一些代码进行一些调整,首先是cursor = lastRet;,这里的lastRet是上一次返回的元素,这里就是 1,所以cursor又变成了 1,这时我们会发现,cursor与size是相等的,所以不会有下一轮循环了。
然后是expectedModCount = modCount;,将expectedModCount与modCount进行了同步,这次就算还有下一轮循环,也不会有ConcurrentModificationException异常了。
三、使用 for 循环遍历集合
1、使用示例
我们知道可以通过集合的 size() 方法获取集合的元素个数,我们其实也可以通过 for 循环来配合这个 size() 方法来遍历集合。但是这个仅限于可以使用 get(int index) 方法的集合结构。
import java.util.ArrayList;
public class Example {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Hello");
list.add("World");
list.add("Hello,World!");
for(int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
运行结果:
Hello
World
Hello,World!
但是,依旧不能直接在 for 循环中使用集合的 remove() 方法。
2、不建议在 for 循环中使用集合的 remove() 方法
因为一旦调用了 remove() 方法,size 就会变化,而 cursor 不会被调整,这时,就可能导致某个元素被跳过。
ArrayList<Character> list = new ArrayList<Character>();
list.add('A');
list.add('B');
list.add('C');
list.add('D');
list.add('E');
for(int i = 0; i < list.size(); i++) {
if(i == 2) {
list.remove(2);
}
}
对于这段代码,在执行 for 循环时,在 i 等于 2 的循环时,会执行 if 语句,会将索引为 2 的元素删除,也是 'C',这时,size 变为了 4,'C' 元素后面的元素的索引都减一。然后进行下一轮循环,这时 i 会自增一,变成了 3,这时,就会有一个元素被跳过了,如下图:

虽然 'D' 会在删除 'C' 的循环的最后被短暂指向了一下,但这是没有意义的,因为这一短暂的指向没法对 'D' 进行单独的操作。
或者,我们对上面的代码进行改动:
for(int i = 0; i < list.size(); i++) {
Character ch = list.get(i);
System.out.println(ch);
if(i == 2) {
list.remove(2);
}
}
对于这段代码,Character ch = list.get(i); 这一句中,ch 不会被赋值成 'D' 这个元素值。最终打印结果中也没有 'D' 元素。
打印结果:
A
B
C
E
四、总结
- 在需要在循环中删除元素时,优先考虑使用
Iterator的remove()方法,以避免并发修改异常。 - 对于只读操作,可以使用
for-each循环,它更简洁。 - 如果需要访问索引或进行复杂的逻辑处理,使用传统的
for循环,但需谨慎处理集合的修改。
——Collection 元素遍历的三种方式&spm=1001.2101.3001.5002&articleId=144171860&d=1&t=3&u=6bef1c7f3881487c8f7b1c7f8d387ef4)
1253

被折叠的 条评论
为什么被折叠?



