Java——集合框架(5)——Collection 元素遍历的三种方式

一、使用 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 的一个内部类 ItrItr 实现了 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;

我们知道 ArrayListAbstractList 类的子类,这个属性又是 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,这时我们会发现,cursorsize 是相等的,所以不会有下一轮循环了。
    然后是 expectedModCount = modCount;,将 expectedModCountmodCount 进行了同步,这次就算还有下一轮循环,也不会有 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

四、总结

  • 在需要在循环中删除元素时,优先考虑使用 Iteratorremove() 方法,以避免并发修改异常。
  • 对于只读操作,可以使用 for-each 循环,它更简洁。
  • 如果需要访问索引或进行复杂的逻辑处理,使用传统的 for 循环,但需谨慎处理集合的修改。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值