MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java ArrayList 的迭代器使用

2022-08-276.9k 阅读

Java ArrayList 的迭代器基础概念

在Java编程中,ArrayList 是一种常用的动态数组实现,它允许我们灵活地存储和管理一组对象。而迭代器(Iterator)则是一种用于遍历集合(如 ArrayList)中元素的工具。迭代器提供了一种统一的方式来访问集合中的元素,而无需关心集合的内部实现细节。

迭代器模式的核心思想是将集合对象的遍历操作从集合类中分离出来,封装到一个独立的迭代器对象中。这样,集合类只需要负责存储和管理元素,而迭代器对象则负责遍历这些元素。这种分离使得代码更加模块化和可维护,同时也方便了不同类型集合的遍历操作。

在Java中,ArrayList 实现了 List 接口,而 List 接口继承自 Collection 接口。Collection 接口定义了 iterator() 方法,该方法返回一个 Iterator 对象,用于遍历集合中的元素。Iterator 接口定义了三个主要方法:hasNext()next()remove()

  • hasNext() 方法:用于判断集合中是否还有下一个元素。如果有,则返回 true;否则,返回 false
  • next() 方法:返回集合中的下一个元素,并将迭代器的位置向前移动一位。如果没有下一个元素,则抛出 NoSuchElementException 异常。
  • remove() 方法:用于从集合中移除当前迭代器所指向的元素。该方法只能在调用 next() 方法之后调用,并且只能调用一次。如果在没有调用 next() 方法之前调用 remove() 方法,或者在调用 next() 方法之后多次调用 remove() 方法,则会抛出 IllegalStateException 异常。

使用迭代器遍历 ArrayList

下面通过一个简单的示例代码来演示如何使用迭代器遍历 ArrayList

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListIteratorExample {
    public static void main(String[] args) {
        // 创建一个 ArrayList 并添加元素
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 获取迭代器
        Iterator<String> iterator = list.iterator();

        // 使用迭代器遍历 ArrayList
        while (iterator.hasNext()) {
            String element = iterator.next();
            System.out.println(element);
        }
    }
}

在上述代码中,首先创建了一个 ArrayList 并添加了三个字符串元素。然后通过调用 list.iterator() 方法获取一个 Iterator 对象。接着,使用 while 循环和 iterator.hasNext() 方法来判断是否还有下一个元素,如果有,则通过 iterator.next() 方法获取下一个元素并打印出来。

这种遍历方式与使用传统的 for 循环遍历 ArrayList 有所不同。使用迭代器遍历的好处在于,它提供了一种统一的方式来遍历不同类型的集合,而无需关心集合的具体实现。此外,迭代器还支持在遍历过程中删除元素,这在某些场景下非常有用。

在迭代过程中删除元素

前面提到,Iterator 接口提供了 remove() 方法,允许我们在遍历集合的过程中删除当前迭代器所指向的元素。下面是一个示例代码,展示了如何在迭代 ArrayList 时删除元素:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListIteratorRemoveExample {
    public static void main(String[] args) {
        // 创建一个 ArrayList 并添加元素
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 获取迭代器
        Iterator<String> iterator = list.iterator();

        // 使用迭代器遍历并删除元素
        while (iterator.hasNext()) {
            String element = iterator.next();
            if ("Banana".equals(element)) {
                iterator.remove();
            }
        }

        // 打印删除元素后的 ArrayList
        System.out.println(list);
    }
}

在上述代码中,在遍历 ArrayList 的过程中,通过判断当前元素是否为 "Banana",如果是,则调用 iterator.remove() 方法将其从集合中删除。最后打印删除元素后的 ArrayList,可以看到 "Banana" 已经被成功删除。

需要注意的是,如果在遍历集合时使用 ArrayListremove() 方法直接删除元素,而不是使用迭代器的 remove() 方法,可能会导致 ConcurrentModificationException 异常。这是因为在使用迭代器遍历集合时,迭代器会维护一个内部的版本号,当集合结构发生变化(如调用 ArrayListremove() 方法)时,版本号会发生改变,而迭代器在调用 next()remove() 方法时会检查版本号是否一致,如果不一致,则抛出 ConcurrentModificationException 异常。

增强型 for 循环与迭代器

在Java 5.0 引入了增强型 for 循环(也称为 for - each 循环),它为遍历集合提供了一种更简洁的语法。增强型 for 循环实际上是基于迭代器实现的,编译器会将增强型 for 循环转换为使用迭代器的代码。

下面是一个使用增强型 for 循环遍历 ArrayList 的示例:

import java.util.ArrayList;
import java.util.List;

public class ArrayListForEachExample {
    public static void main(String[] args) {
        // 创建一个 ArrayList 并添加元素
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 使用增强型 for 循环遍历 ArrayList
        for (String element : list) {
            System.out.println(element);
        }
    }
}

在上述代码中,使用增强型 for 循环遍历 ArrayList,代码更加简洁明了。但是需要注意的是,增强型 for 循环不支持在遍历过程中删除元素。如果需要在遍历过程中删除元素,仍然需要使用迭代器的 remove() 方法。

例如,如果尝试在增强型 for 循环中使用 ArrayListremove() 方法删除元素,会导致 ConcurrentModificationException 异常:

import java.util.ArrayList;
import java.util.List;

public class ArrayListForEachRemoveErrorExample {
    public static void main(String[] args) {
        // 创建一个 ArrayList 并添加元素
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 尝试在增强型 for 循环中删除元素,会抛出 ConcurrentModificationException
        for (String element : list) {
            if ("Banana".equals(element)) {
                list.remove(element);
            }
        }
    }
}

为了在遍历过程中安全地删除元素,应使用迭代器:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListIteratorRemoveSafeExample {
    public static void main(String[] args) {
        // 创建一个 ArrayList 并添加元素
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 使用迭代器遍历并删除元素
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String element = iterator.next();
            if ("Banana".equals(element)) {
                iterator.remove();
            }
        }

        // 打印删除元素后的 ArrayList
        System.out.println(list);
    }
}

双向迭代器 ListIterator

除了 Iterator 接口,Java 还提供了 ListIterator 接口,它继承自 Iterator 接口,专门用于遍历 List 类型的集合,如 ArrayListListIterator 接口提供了一些额外的方法,使得在遍历 List 时更加灵活。

ListIterator 接口新增的主要方法有:

  • hasPrevious() 方法:用于判断是否还有前一个元素。如果有,则返回 true;否则,返回 false
  • previous() 方法:返回前一个元素,并将迭代器的位置向后移动一位。如果没有前一个元素,则抛出 NoSuchElementException 异常。
  • nextIndex() 方法:返回下一个元素的索引。
  • previousIndex() 方法:返回前一个元素的索引。
  • add(E e) 方法:在当前位置插入指定元素。
  • set(E e) 方法:用指定元素替换上次调用 next()previous() 方法返回的元素。

下面是一个使用 ListIterator 遍历 ArrayList 的示例代码,展示了如何使用这些新增方法:

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class ArrayListListIteratorExample {
    public static void main(String[] args) {
        // 创建一个 ArrayList 并添加元素
        List<String> list = new ArrayList<>();
        list.add("Apple");
        list.add("Banana");
        list.add("Cherry");

        // 获取 ListIterator
        ListIterator<String> listIterator = list.listIterator();

        // 正向遍历并打印元素
        System.out.println("正向遍历:");
        while (listIterator.hasNext()) {
            String element = listIterator.next();
            System.out.println(element);
        }

        // 反向遍历并打印元素
        System.out.println("反向遍历:");
        while (listIterator.hasPrevious()) {
            String element = listIterator.previous();
            System.out.println(element);
        }

        // 在指定位置插入元素
        listIterator = list.listIterator(1);
        listIterator.add("Date");

        // 打印插入元素后的 ArrayList
        System.out.println("插入元素后:");
        for (String element : list) {
            System.out.println(element);
        }

        // 修改指定元素
        listIterator = list.listIterator();
        while (listIterator.hasNext()) {
            String element = listIterator.next();
            if ("Date".equals(element)) {
                listIterator.set("Fig");
            }
        }

        // 打印修改元素后的 ArrayList
        System.out.println("修改元素后:");
        for (String element : list) {
            System.out.println(element);
        }
    }
}

在上述代码中,首先获取了一个 ListIterator 对象,然后分别进行了正向遍历和反向遍历。接着,使用 add() 方法在指定位置插入了一个元素,最后使用 set() 方法修改了指定元素。通过这些操作,可以看到 ListIterator 提供了比 Iterator 更丰富的功能,适用于需要在遍历 List 时进行更多操作的场景。

迭代器的内部实现原理

了解迭代器的内部实现原理有助于我们更好地理解其工作机制和性能特点。在 ArrayList 中,迭代器的实现是基于数组的索引。

当调用 ArrayListiterator() 方法时,会创建一个 Itr 内部类的实例,该类实现了 Iterator 接口。Itr 类维护了一个 cursor 变量,用于记录当前迭代的位置,初始值为 0。同时,还维护了一个 lastRet 变量,用于记录上一次调用 next() 方法返回的元素的索引,初始值为 -1。

hasNext() 方法的实现很简单,只需判断 cursor 是否小于 ArrayList 的大小即可:

public boolean hasNext() {
    return cursor != size;
}

next() 方法会先检查 cursor 是否超过了 ArrayList 的大小,如果超过则抛出 NoSuchElementException 异常。然后,返回当前位置的元素,并将 cursor 加 1,同时更新 lastRet 为当前位置:

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];
}

remove() 方法则会检查 lastRet 是否为 -1,如果是,则抛出 IllegalStateException 异常。然后,调用 ArrayListremove() 方法删除指定索引的元素,并更新 cursorlastRet

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();
    }
}

其中,checkForComodification() 方法用于检查 ArrayList 的结构是否发生变化。在 ArrayList 中,每次对集合结构进行修改(如添加、删除元素)时,modCount 变量会增加。迭代器在调用 next()remove() 方法时,会检查 expectedModCount 是否与 modCount 相等,如果不相等,则抛出 ConcurrentModificationException 异常,以保证在遍历过程中集合结构的一致性。

ListIterator 的实现类似,只不过它是由 ListItr 内部类实现,该类继承自 Itr 类,并实现了 ListIterator 接口。ListItr 类增加了对反向遍历、插入和修改元素等功能的支持。

迭代器的性能考虑

在使用迭代器遍历 ArrayList 时,性能是一个需要考虑的因素。一般来说,使用迭代器遍历 ArrayList 的性能与使用传统的 for 循环遍历的性能相近。

使用迭代器遍历的优势在于其代码的简洁性和通用性,特别是在处理不同类型的集合时。然而,由于迭代器内部维护了一些状态变量(如 cursorlastRet),并且每次调用 next() 方法时都需要进行边界检查和 modCount 检查,因此在某些极端情况下,可能会比直接使用 for 循环略慢。

例如,在对一个非常大的 ArrayList 进行简单的顺序遍历,并且不需要在遍历过程中删除元素时,使用传统的 for 循环可能会有更好的性能:

import java.util.ArrayList;
import java.util.List;

public class ArrayListPerformanceComparison {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 1000000; i++) {
            list.add(i);
        }

        long startTime = System.currentTimeMillis();
        for (int i = 0; i < list.size(); i++) {
            Integer element = list.get(i);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("使用 for 循环遍历时间: " + (endTime - startTime) + " 毫秒");

        startTime = System.currentTimeMillis();
        for (Integer element : list) {
        }
        endTime = System.currentTimeMillis();
        System.out.println("使用增强型 for 循环遍历时间: " + (endTime - startTime) + " 毫秒");
    }
}

在上述代码中,分别使用传统的 for 循环和增强型 for 循环(基于迭代器)对一个包含 100 万个元素的 ArrayList 进行遍历,并比较它们的执行时间。在实际测试中,可能会发现传统的 for 循环在这种情况下性能略好一些。

但是,当需要在遍历过程中删除元素,或者处理不同类型的集合时,使用迭代器的优势就会体现出来。而且,现代的Java编译器和虚拟机对迭代器的实现进行了优化,在大多数实际应用场景中,迭代器的性能与传统 for 循环的性能差异并不明显。

并发环境下的迭代器使用

在多线程并发访问 ArrayList 时,使用迭代器需要特别小心。由于 ArrayList 本身不是线程安全的,在多线程环境下同时对 ArrayList 进行遍历和修改操作,很容易导致 ConcurrentModificationException 异常。

例如,假设有两个线程,一个线程在遍历 ArrayList,另一个线程在向 ArrayList 中添加元素:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListConcurrentModificationExample {
    private static List<String> list = new ArrayList<>();

    public static void main(String[] args) {
        list.add("Apple");
        list.add("Banana");

        Thread thread1 = new Thread(() -> {
            Iterator<String> iterator = list.iterator();
            while (iterator.hasNext()) {
                String element = iterator.next();
                System.out.println("线程1遍历元素: " + element);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add("Cherry");
            System.out.println("线程2添加元素: Cherry");
        });

        thread1.start();
        thread2.start();
    }
}

在上述代码中,thread1 线程在遍历 ArrayListthread2 线程在 thread1 遍历过程中向 ArrayList 中添加了一个元素。运行这段代码,很可能会抛出 ConcurrentModificationException 异常。

为了在并发环境下安全地使用迭代器遍历 ArrayList,可以采取以下几种方法:

使用线程安全的集合类

Java 提供了一些线程安全的集合类,如 CopyOnWriteArrayListCopyOnWriteArrayList 在进行写操作(如添加、删除元素)时,会创建一个新的数组,并将原数组的内容复制到新数组中,然后在新数组上进行操作。而读操作(如遍历)则直接使用原数组,这样就避免了在遍历过程中数组结构的变化,从而避免了 ConcurrentModificationException 异常。

下面是使用 CopyOnWriteArrayList 的示例:

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {
    private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) {
        list.add("Apple");
        list.add("Banana");

        Thread thread1 = new Thread(() -> {
            Iterator<String> iterator = list.iterator();
            while (iterator.hasNext()) {
                String element = iterator.next();
                System.out.println("线程1遍历元素: " + element);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add("Cherry");
            System.out.println("线程2添加元素: Cherry");
        });

        thread1.start();
        thread2.start();
    }
}

在上述代码中,使用 CopyOnWriteArrayList 代替了 ArrayList,这样在多线程环境下遍历和修改集合时就不会抛出 ConcurrentModificationException 异常。不过,需要注意的是,CopyOnWriteArrayList 的写操作性能相对较低,因为每次写操作都需要复制数组,适用于读多写少的场景。

使用同步机制

另一种方法是使用同步机制,如 synchronized 关键字或 ReentrantLock,来保证在同一时间只有一个线程可以访问 ArrayList

下面是使用 synchronized 关键字的示例:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListSynchronizedExample {
    private static List<String> list = new ArrayList<>();

    public static void main(String[] args) {
        list.add("Apple");
        list.add("Banana");

        Thread thread1 = new Thread(() -> {
            synchronized (list) {
                Iterator<String> iterator = list.iterator();
                while (iterator.hasNext()) {
                    String element = iterator.next();
                    System.out.println("线程1遍历元素: " + element);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (list) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                list.add("Cherry");
                System.out.println("线程2添加元素: Cherry");
            }
        });

        thread1.start();
        thread2.start();
    }
}

在上述代码中,通过 synchronized 关键字对 list 对象进行同步,确保在同一时间只有一个线程可以访问 ArrayList,从而避免了 ConcurrentModificationException 异常。这种方法虽然可以保证线程安全,但在高并发情况下,由于线程之间的竞争锁,可能会导致性能下降。

总结

迭代器是Java中遍历集合(如 ArrayList)的重要工具,它提供了一种统一、灵活且方便的方式来访问集合中的元素。通过本文的介绍,我们深入了解了 ArrayList 迭代器的基本概念、使用方法、内部实现原理、性能考虑以及在并发环境下的使用。

在实际编程中,应根据具体的需求选择合适的遍历方式。如果只需要简单地顺序遍历集合,并且不需要在遍历过程中删除元素,传统的 for 循环或增强型 for 循环可能是不错的选择;如果需要在遍历过程中删除元素,或者需要处理不同类型的集合,使用迭代器则更为合适。在并发环境下,要注意选择合适的线程安全策略,以避免 ConcurrentModificationException 异常的发生。

希望通过本文的讲解,能帮助读者更好地掌握 Java ArrayList 迭代器的使用,写出更加健壮、高效的Java代码。