Java迭代器探秘:为什么它能安全地"边吃边扔"? 🍪🗑️
迭代器的"超能力"清单 💪
- 安全删除:能在遍历时删除元素而不引发
ConcurrentModificationException
- 统一访问:不管什么集合,Iterator提供统一的操作方式
- 懒加载:元素只在需要时才被获取
- 状态感知:知道自己在集合中的位置
为什么普通操作会爆炸?💥
先看这段作死代码:
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++
- 创建迭代器时,会记录当前的
modCount
为expectedModCount
- 每次操作前检查这两个值是否一致
java
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
2. 迭代器的删除是"官方认证"的 ✔️
iterator.remove()
的特殊之处:
- 它会在删除后同步更新
expectedModCount = modCount
- 保持游标位置正确
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(); // 安全删除!
}
}
这就像:
- 你告诉迭代器:"我要删除当前这个"
- 迭代器帮你处理好所有内部状态
- 保证后续遍历不会乱套
不同集合迭代器的秘密武器 🗡️
ArrayList的迭代器:
- 基于数组索引
- 删除后需要移动后面元素
LinkedList的迭代器:
- 基于节点指针
- 删除只需调整前后节点引用
ConcurrentHashMap的迭代器:
- 弱一致性迭代器
- 可以容忍并发修改
- 不保证能反映所有最新修改
迭代器的限制 🚧
- 不能添加元素:迭代器只有remove()没有add()
- 单线程操作:虽然单个迭代器安全,但多个迭代器同时修改还是会出问题
- 部分集合不支持:比如某些第三方实现的不可变集合
趣味实验:自己实现一个迭代器 🔬
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();
}
}
}
总结:迭代器的安全之道 🛡️
- 版本控制:通过
modCount
和expectedModCount
的对比检测非法修改 - 状态维护:删除后正确调整游标位置
- 官方通道:只有通过迭代器自身的remove()方法才能安全删除
- 设计模式:遵循"单一职责原则",把遍历和修改的逻辑封装在一起
所以记住:下次要"边吃边扔"时,一定要用迭代器这个"官方垃圾袋"!🗑️✅
补充
for-each与常规for循环的效率区别
在理论上,使用 for-each 循环(也称为增强型 for 循环)和常规 for 循环的效率并没有太大差别。这是因为它们都用于迭代集合或数组,并且编译器在生成字节码时通常会将它们优化为类似的结构。
然而,在一些特定情况下,常规 for 循环可能会稍微快一些,尤其是在迭代数组时,因为 for-each 循环需要额外的指令来获取迭代器并检查是否还有元素可供迭代。而常规 for 循环只需简单地使用索引来访问数组元素。
尽管如此,这种差异通常在实践中是微不足道的,并且在代码的可读性和简洁性方面,for-each 循环通常更胜一筹。因此,在编写代码时,应该优先考虑使用更易读、更简洁的 for-each 循环,除非有特殊需求需要使用常规 for 循环。只有在非常高要求的性能场景下,才会考虑微小的性能差异。