Skip to content

Java迭代器探秘:为什么它能安全地"边吃边扔"? 🍪🗑️

迭代器的"超能力"清单 💪

  1. 安全删除:能在遍历时删除元素而不引发ConcurrentModificationException
  2. 统一访问:不管什么集合,Iterator提供统一的操作方式
  3. 懒加载:元素只在需要时才被获取
  4. 状态感知:知道自己在集合中的位置

为什么普通操作会爆炸?💥

先看这段作死代码:

java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s);  // 嘭!ConcurrentModificationException!
    }
}

这就像你在数钱时,你妈突然抽走一张钞票,你还怎么保持计数准确?💰➡️👋

迭代器的安全删除原理 🛡️

1. 版本号控制 - 集合的"防伪标识" 🏷️

每个集合内部维护一个modCount(修改计数器):

  • 每次结构性修改(增删)都会使modCount++
  • 创建迭代器时,会记录当前的modCountexpectedModCount
  • 每次操作前检查这两个值是否一致
java
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

2. 迭代器的删除是"官方认证"的 ✔️

iterator.remove()的特殊之处:

  1. 它会在删除后同步更新expectedModCount = modCount
  2. 保持游标位置正确
java
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);  // 调用集合的remove
        cursor = lastRet;                // 调整游标
        lastRet = -1;
        expectedModCount = modCount;     // 关键!同步版本号
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

迭代器删除的"正确姿势" 🧘

java
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if ("B".equals(s)) {
        it.remove();  // 安全删除!
    }
}

这就像:

  1. 你告诉迭代器:"我要删除当前这个"
  2. 迭代器帮你处理好所有内部状态
  3. 保证后续遍历不会乱套

不同集合迭代器的秘密武器 🗡️

ArrayList的迭代器:

  • 基于数组索引
  • 删除后需要移动后面元素

LinkedList的迭代器:

  • 基于节点指针
  • 删除只需调整前后节点引用

ConcurrentHashMap的迭代器:

  • 弱一致性迭代器
  • 可以容忍并发修改
  • 不保证能反映所有最新修改

迭代器的限制 🚧

  1. 不能添加元素:迭代器只有remove()没有add()
  2. 单线程操作:虽然单个迭代器安全,但多个迭代器同时修改还是会出问题
  3. 部分集合不支持:比如某些第三方实现的不可变集合

趣味实验:自己实现一个迭代器 🔬

java
public class MyList<E> implements Iterable<E> {
    private Object[] data = new Object[10];
    private int size = 0;
    private int modCount = 0;  // 我们的版本号

    // 添加元素方法
    public void add(E e) {
        data[size++] = e;
        modCount++;
    }

    // 实现迭代器
    @Override
    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        private int cursor = 0;
        private int lastRet = -1;
        private int expectedModCount = modCount;  // 记录创建时的版本号

        @Override
        public boolean hasNext() {
            return cursor < size;
        }

        @Override
        public E next() {
            checkForComodification();
            if (cursor >= size) throw new NoSuchElementException();
            lastRet = cursor;
            return (E) data[cursor++];
        }

        @Override
        public void remove() {
            if (lastRet < 0) throw new IllegalStateException();
            checkForComodification();

            System.arraycopy(data, lastRet+1, data, lastRet, size-lastRet-1);
            size--;
            cursor = lastRet;  // 游标回退
            lastRet = -1;
            expectedModCount = modCount;  // 同步版本号
        }

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

总结:迭代器的安全之道 🛡️

  1. 版本控制:通过modCountexpectedModCount的对比检测非法修改
  2. 状态维护:删除后正确调整游标位置
  3. 官方通道:只有通过迭代器自身的remove()方法才能安全删除
  4. 设计模式:遵循"单一职责原则",把遍历和修改的逻辑封装在一起

所以记住:下次要"边吃边扔"时,一定要用迭代器这个"官方垃圾袋"!🗑️✅

补充

for-each与常规for循环的效率区别

在理论上,使用 for-each 循环(也称为增强型 for 循环)和常规 for 循环的效率并没有太大差别。这是因为它们都用于迭代集合或数组,并且编译器在生成字节码时通常会将它们优化为类似的结构。

然而,在一些特定情况下,常规 for 循环可能会稍微快一些,尤其是在迭代数组时,因为 for-each 循环需要额外的指令来获取迭代器并检查是否还有元素可供迭代。而常规 for 循环只需简单地使用索引来访问数组元素。

尽管如此,这种差异通常在实践中是微不足道的,并且在代码的可读性和简洁性方面,for-each 循环通常更胜一筹。因此,在编写代码时,应该优先考虑使用更易读、更简洁的 for-each 循环,除非有特殊需求需要使用常规 for 循环。只有在非常高要求的性能场景下,才会考虑微小的性能差异。

技术漫游

本站访客数 人次 本站总访问量