Iterator、ListIterator 使用时的注意点

本文详细介绍了Java集合框架中的Iterator和ListIterator接口,包括它们的功能、使用方法以及常见陷阱。重点讲解了next()、previous()、remove()、set()和add()等操作的细节,并通过代码示例展示了这些方法的正确和错误用法。同时,分析了ArrayList和LinkedList中Iterator的实现,强调了并发修改检查和游标管理的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Iterator 是集合框架的重要组成部分,用来封装集合的遍历。ListIterator 扩展了 Iterator,添加了一些针对 List 的方法,比如向前遍历、添加、更新。同时迭代器在使用时会有一些坑。

Iterator

简介、测试

public interface Iterator<E> {
    /**
     * 迭代器没有到达末尾则返回 true
     */
    boolean hasNext();

    /**
     * 返回下一个元素,或者抛出异常 NoSuchElementException(已经到达末尾时)
     */
    E next();

    /**
     * 删除迭代器最后一次返回的元素,或者抛出异常 IllegalStateException 
     */
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    /**
     * 对每个剩余元素执行给定的操作
     */
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

remove() 方法需要特别注意,它的功能是删除迭代器最后一次返回的元素,且每次调用 next() 方法后只能调用一次 remove() 方法,最后一次返回的元素指的是:

  • next() 方法返回的元素
  • forEachRemaining() 迭代的最后一个元素,大概率是集合的最后一个元素(但发生了并发修改就可能不是集合的最后一个元素了,详见下面的源码分析)

remove 注意点:

  • 调用 remove() 方法前必须先调用 next()forEachRemaining() 方法
  • 每次 next() 调用后最多只能调用一次 remove() 方法

以下代码会报错,因为调用 remove() 方法前没有调用 next() 方法,导致不知道要删除哪个元素:

public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4);
    Iterator<Integer> iterator = list.iterator();
    iterator.remove();
}
Exception in thread "main" java.lang.IllegalStateException
	at java.util.ArrayList$Itr.remove(ArrayList.java:874)
	at com.example.heima.itr.ItrTest.main(ItrTest.java:21)

以下代码同样会报错(在第二次调用 remove() 方法时),即使先调用了两次 next() 方法,因为 remove() 方法只会删除上一次返回的元素,不会关注上上次返回的:

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
    Iterator<Integer> iterator = list.iterator();
    iterator.next();
    iterator.next();
    iterator.remove();
    System.out.println(list);
    iterator.remove();
}
[1, 3, 4]
Exception in thread "main" java.lang.IllegalStateException
	at java.util.ArrayList$Itr.remove(ArrayList.java:874)
	at com.example.heima.itr.ItrTest.main(ItrTest.java:23)

以下代码会正常执行:

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
    Iterator<Integer> iterator = list.iterator();
    iterator.next();
    iterator.remove();
    System.out.println(list);
    iterator.next();
    iterator.remove();
    System.out.println(list);
}
[2, 3, 4]
[3, 4]

以下代码会删除最后一个元素 “4”:

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
    Iterator<Integer> iterator = list.iterator();
    iterator.forEachRemaining(System.out::println);
    iterator.remove();
    System.out.println(list);
}
1
2
3
4
[1, 2, 3]

ArrayList 的 Iterator 实现

可以通过 ArrayList 的源码来更好的理解 Iterator,使用 lastRet 记录最后一次返回的元素的索引,默认值为 -1 代表没有最后一次返回的元素。next()forEachRemaining() 方法会修改 lastRetremove() 方法会使用并重置 lastRet

private class Itr implements Iterator<E> {
    int cursor;       // 下一个元素的索引
    int lastRet = -1; // 最后一次返回的元素的索引,若没有则返回 -1
    int expectedModCount = modCount;	// 实例化迭代器时会记录当前 ArrayList 的 modCount,用于并发修改的检查

    Itr() {}

    // 下一个元素的索引为 size 说明到末尾了
    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        // 并发修改检查
        checkForComodification();
        // 记录当前的 cursor
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        // cursor++
        cursor = i + 1;
        // 使用 lastRet 记录加一之前的 cursor,即上次返回的元素的索引,由于 remove
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        // 之前没有调用过 next() 方法,
        // 或之前调用过 next() 方法,但之后已经调用过 remove() 了,因为 remove() 会将 lastRet 重置为 -1
        if (lastRet < 0)
            throw new IllegalStateException();
        // 并发修改检查
        checkForComodification();

        try {
            // 删除上次返回的元素 lastRet
            ArrayList.this.remove(lastRet);
            // cursor--
            cursor = lastRet;
            // 重置 lastRet
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        // 已经到达末尾
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        // 在迭代的过程中,原始数组被变小了,说明发生了其他的结构化修改
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        // 判断条件有 modCount == expectedModCount,所以遇到并发修改会停止迭代
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        // 更新下一个元素的索引以及最后一次返回的元素的索引
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

ListIterator

简介、测试

是专门用于 List 的迭代器,允许从任意方向(正向、反向)遍历列表,在迭代期间修改列表,并获取迭代器在列表中的当前位置。ListIterator 没有当前元素;它的游标总是位于调用 previous() 返回的元素和调用 next() 返回的元素之间。对于长度为 n 的列表,迭代器有 n+1 个可能的游标位置,如下所示:

                        Element(0)   Element(1)   Element(2)   ... Element(n-1)
   cursor positions:  ^            ^            ^            ^                  ^

注意,remove()set(Object) 方法不是根据游标位置定义的;而是对调用 next()previous()forEachRemaining() 返回的最后一个元素进行操作。

public interface ListIterator<E> extends Iterator<E> {
    // Query Operations

    /**
     * 如果迭代器在正向遍历列表时有更多的元素,则返回 true。
     */
    boolean hasNext();

    /**
     * 返回列表中的下一个元素并推进游标位置。(注意,交替调用 next 和 previous 将重复返回相同的元素。)
     */
    E next();

    /**
     * 如果迭代器在反向遍历列表时有更多的元素,则返回 true
     */
    boolean hasPrevious();

    /**
     * 返回列表中的前一个元素并向后移动游标位置。(注意,交替调用 next 和 previous 将重复返回相同的元素。)
     */
    E previous();

    /**
     * 返回下次调用 next 返回的元素的索引。(如果迭代器位于列表的末尾,则返回列表大小。)
     */
    int nextIndex();

    /**
     * 返回下次调用 previous 返回的元素的索引。(如果迭代器位于列表的开头,则返回 -1。)
     */
    int previousIndex();


    // Modification Operations

    /**
     * 从列表中删除最后一个返回的元素。每次调用 next() 或 previous() 后,只能调用一次 remove()。
     * 只有在上次调用 next() 或 previous() 后没有调用 add() 时,才能进行此操作。
     */
    void remove();

    /**
     * 最后一个返回的元素替换为指定的元素。只有在对next或previous的最后一次调用之后没有调用remove或add时,才能进行此调用。
     * 只有在上次调用 next() 或 previous() 后没有调用 add() 或 remove() 时,才能进行此操作。
     */
    void set(E e);

    /**
     * 将指定的元素插入列表。元素被插入到 next() 返回的元素的前面,以及 previous() 返回的元素的后面。
     * (如果列表中不包含元素,则新元素将成为列表中唯一的元素。)
     * 在隐式游标之前插入新元素:对 next() 的后续调用将不受影响,而对 previous() 的后续调用将返回新元素。
     * (这个调用增加了一个调用 nextIndex()、previousIndex() 返回值。)
     */
    void add(E e);
}

交替调用 next 和 previous 将重复返回相同的元素。

public static void main(String[] args) {
    List<Integer> list = new LinkedList<>(Arrays.asList(1, 2, 3, 4));
    ListIterator<Integer> iterator = list.listIterator(2);
    System.out.println(iterator.next());
    System.out.println(iterator.previous());

    System.out.println(iterator.previous());
    System.out.println(iterator.next());
}
3
3
2
2

以下为隐式游标的变化:

        1   2   3   4
开始:          ^			
next:              ^    3
previous:     ^	        3
previous: ^	            2
next:         ^	        2

remove()

ListIteratorremove() 方法与 Iterator 相似,都是删除最后一个返回的元素。最后一次返回的元素可能是:

  • next() 方法返回的元素
  • previous() 方法返回的元素
  • forEachRemaining() 迭代的最后一个元素

注意点:

  • 调用 remove() 方法前必须先调用 next()previous()forEachRemaining() 方法,且在上次调用上述方法后没有调用 add() ,才能执行删除。因为 add() 会推进游标,且会清除最后一个返回的元素。调用 set() 不会影响 remove(),因为 set() 不会清除最后一个返回的元素。
  • 每次以上方法调用后最多只能调用一次 remove() 方法

以下代码会报错:因为 add() 会推进游标,且会清除最后一个返回的元素,导致 remove() 找不到模目标

public static void main(String[] args) {
    List<Integer> list = new LinkedList<>(Arrays.asList(1, 2, 3, 4));
    ListIterator<Integer> iterator = list.listIterator();
    iterator.next();
    iterator.next();
    iterator.add(0);
    System.out.println(list);
    iterator.remove();
}
Exception in thread "main" java.lang.IllegalStateException
	at java.util.LinkedList$ListItr.remove(LinkedList.java:923)
	at com.example.heima.itr.ItrTest.main(ItrTest.java:20)

set()

set() 功能是替换最后一个返回的元素,前提是最后一个返回的元素必须存在,否则抛异常,和 remove() 的约束类似:

  • 调用 set() 方法前必须先调用 next()previous()forEachRemaining() 方法,且在上次调用上述方法后没有调用 add()remove(),才能执行此操作。
  • remove() 不同的是,set() 方法可多次调用

以下代码正常执行,因为 set() 方法可多次调用:

public static void main(String[] args) {
    List<Integer> list = new LinkedList<>(Arrays.asList(1, 2, 3, 4));
    ListIterator<Integer> iterator = list.listIterator();
    iterator.next();
    System.out.println(list);
    iterator.set(100);
    System.out.println(list);
    iterator.set(200);
    System.out.println(list);
}
[1, 2, 3, 4]
[100, 2, 3, 4]
[200, 2, 3, 4]

以下代码会报错,因为 remove() 会将游标左移,并清除最后一个返回的元素:

public static void main(String[] args) {
    List<Integer> list = new LinkedList<>(Arrays.asList(1, 2, 3, 4));
    ListIterator<Integer> iterator = list.listIterator();
    iterator.next();
    iterator.next();
    System.out.println(list);
    iterator.remove();
    System.out.println(list);
    iterator.set(100);
    System.out.println(list);
}
[1, 2, 3, 4]
[1, 3, 4]
Exception in thread "main" java.lang.IllegalStateException
	at java.util.LinkedList$ListItr.set(LinkedList.java:937)
	at com.example.heima.itr.ItrTest.main(ItrTest.java:21)

将上面的 remove 换成 add() 也会报错,因为 add() 会推进游标,且清除最后一个返回的元素:

public static void main(String[] args) {
    List<Integer> list = new LinkedList<>(Arrays.asList(1, 2, 3, 4));
    ListIterator<Integer> iterator = list.listIterator();
    iterator.next();
    iterator.next();
    System.out.println(list);
    iterator.add(0);
    System.out.println(list);
    iterator.set(100);
    System.out.println(list);
}
[1, 2, 3, 4]
[1, 2, 0, 3, 4]
Exception in thread "main" java.lang.IllegalStateException
	at java.util.LinkedList$ListItr.set(LinkedList.java:937)
	at com.example.heima.itr.ItrTest.main(ItrTest.java:21)

add()

  • 元素被插入到 next() 返回的元素的前面,以及 previous() 返回的元素的后面。
  • 如果列表中不包含元素,则新元素将成为列表中唯一的元素。
  • 在隐式游标之前插入新元素:对 next() 的后续调用将不受影响,而对 previous() 的后续调用将返回新元素。
  • 这个调用增加了一个调用 nextIndex()、previousIndex() 返回值。
public static void main(String[] args) {
    List<Integer> list = new LinkedList<>(Arrays.asList(1, 2, 3, 4));
    ListIterator<Integer> iterator = list.listIterator();
    System.out.println(list);
    iterator.next();
    System.out.println("nextIndex:" + iterator.nextIndex());
    System.out.println("previousIndex:" + iterator.previousIndex());
    iterator.add(0);
    System.out.println(list);
    System.out.println("nextIndex:" + iterator.nextIndex());
    System.out.println("previousIndex:" + iterator.previousIndex());
    System.out.println("next:" + iterator.next());
    System.out.println("previous:" + iterator.previous());
}
[1, 2, 3, 4]
nextIndex:1
previousIndex:0
[1, 0, 2, 3, 4]
nextIndex:2
previousIndex:1
next:2
previous:2

游标的变化:

            1   2   3   4
开始:      ^	 			
next:	      ^					
            1   2   3   4
add:         0^						// 在游标的前面插入,游标前面的元素多了一个,所以要推进游标
            1   0	2   3   4
				  ^
next:                 ^			2
previous:         ^				2	

ArrayList 的 ListIterator 实现

private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        super();
        cursor = index;
    }

    // cursor 代表下一个元素的索引,cursor == 0 说明已经到达头部了,一个没有上一个了
    public boolean hasPrevious() {
        return cursor != 0;
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor - 1;
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        // 并发修改检查
        checkForComodification();
        // cursor 代表下一个元素的索引
        // 隐式游标始终位于 previous 与 next 之间
        // 索引 previous = next - 1 = cursor - 1
        int i = cursor - 1;
        if (i < 0)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i;
        // 记录上次返回的元素的索引 lastRet
        return (E) elementData[lastRet = i];
    }

    public void set(E e) {
        // 判断最后一次返回的元素是否存在
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            // 更新,且没有重置 lastRet,所以能够重复调用 set
            ArrayList.this.set(lastRet, e);
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    public void add(E e) {
        checkForComodification();

        try {
            int i = cursor;
            ArrayList.this.add(i, e);
            // 推进游标
            cursor = i + 1;
            // 重置 lastRet,清除最后一次返回的元素
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}

总结

  • 游标总是位于调用 previous() 返回的元素和调用 next() 返回的元素之间

  • 交替调用 next()previous() 将重复返回相同的元素

  • remove()set() 需要最后一次返回的元素

  • next()previous()forEachRemaining() 会创建最后一次返回的元素

  • remove()add() 会清除最后一次返回的元素

  • set() 可多次调用

  • add() 在隐式游标的前面插入,并推进游标

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值