《设计模式之美》迭代器模式(中):遍历集合的同时,为什么不能增删集合元素?

王争《设计模式之美》学习笔记

在遍历的同时增删集合元素会发生什么?

  • 在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。
  • 结果不可预期行为或者未决行为:运行结果到底是对还是错,要视情况而定。

文中举例,删除元素

public interface Iterator<E> {
  boolean hasNext();
  void next();
  E currentItem();
}

public class ArrayIterator<E> implements Iterator<E> {
  private int cursor;
  private ArrayList<E> arrayList;
  public ArrayIterator(ArrayList<E> arrayList) {
    this.cursor = 0;
    this.arrayList = arrayList;
  }
  @Override
  public boolean hasNext() {
    return cursor < arrayList.size();
  }
  @Override
  public void next() {
    cursor++;
  }
  @Override
  public E currentItem() {
    if (cursor >= arrayList.size()) {
      throw new NoSuchElementException();
    }
    return arrayList.get(cursor);
  }
}

public interface List<E> {
  Iterator iterator();
}

public class ArrayList<E> implements List<E> {
  //...
  public Iterator iterator() {
    return new ArrayIterator(this);
  }
  //...
}

public class Demo {
  public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("a");
    names.add("b");
    names.add("c");
    names.add("d");
    Iterator<String> iterator = names.iterator();
    iterator.next();
    names.remove("a");
  }
}
  • ArrayList 底层对应的是数组这种数据结构,在执行完第55行代码的时候,数组中存储的是 a、b、c、d 四个元素,迭代器的游标 cursor 指向元素a。
  • 当执行完第56行代码的时候,游标指向元素b,到这里都没有问题。
  • 执行到第57行代码的时候,我们从数组中将元素a 删除掉,b、c、d 三个元素会依次往前搬移一位,这就会导致游标本来指向元素b,现在变成了指向元素c。
  • 如果第57行代码删除的不是游标前面的元素(元素a)以及游标所在位置的元素(元素b),而是游标后面的元素(元素c和d),这样就不会存在任何问题了,不会存在某个元素遍历不到的情况了。

文中举例,添加元素

public class Demo {
  public static void main(String[] args) {
    List<String> names = new ArrayList<>();
    names.add("a");
    names.add("b");
    names.add("c");
    names.add("d");
    Iterator<String> iterator = names.iterator();
    iterator.next();
    names.add(0, "x");
  }
}
  • 在执行完第10行代码之后,数组中包含 a、b、c、d 四个元素,游标指向 b 这个元素,已经跳过了元素a。
  • 在执行完第11行代码之后,我们将 x 插入到下标为0的位置,a、b、c、d 四个元素依次往后移动一位。这个时候,游标又重新指向了元素a。元素a 被游标重复指向两次,也就是说,元素a 存在被重复遍历的情况。
  • 跟删除情况类似,如果我们在游标的后面添加元素,就不会存在任何问题。

如何应对遍历时改变集合导致的未决行为?

  • 一种是遍历的时候不允许增删元素。
    • 第一种解决方案比较难实现,我们要确定遍历开始和结束的时间点。
    • 遍历开始的时间节点我们很容易获得。我们可以把创建迭代器的时间点作为遍历开始的时间点。
    • 遍历到最后一个元素的时候就算结束不行,在实际的软件开发中,每次使用迭代器来遍历元素,并不一定非要把所有元素都遍历一遍。比如,我们找到一个指定值的元素就提前结束了遍历。
    • 在迭代器类中定义一个新的接口 finishIteration(),主动告知容器迭代器使用完了,你可以增删元素了。但是,这就要求程序员在使用完迭代器之后要主动调用这个函数,也增加了开发成本,还很容易漏掉。
  • 另一种是增删元素之后让遍历报错。
    • 第二种解决方法更加合理。
    • 我们在 ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加1。
    • 当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们把 modCount 值传递给迭代器的 expectedModCount 成员变量,之后每次调用迭代器上的hasNext()、next()、currentItem() 函数,我们都会检查集合上的 modCount 是否等于expectedModCount,也就是看,在创建完迭代器之后,modCount 是否改变过。
    • 如果两个值不相同,那就说明集合存储的元素已经改变了,要么增加了元素,要么删除了元素,之前创建的迭代器已经不能正确运行了,再继续使用就会产生不可预期的结果,所以我们选择 fail-fast 解决方式,抛出运行时异常,结束掉程序,让程序员尽快修复这个因为不正确使用迭代器而产生的 bug。

如何在遍历的同时安全地删除集合元素?

  • 像 Java 语言,迭代器类中除了前面提到的几个最基本的方法之外,还定义了一个 remove() 方法,能够在遍历集合的同时,安全地删除集合中的元素。不过,需要说明的是,它并没有提供添加元素的方法。毕竟迭代器的主要作用是遍历,添加元素放到迭代器里本身就不合适。
  • Java迭代器中提供的 remove() 方法作用有限。它只能删除游标指向的前一个元素,而且一个 next() 函数之后,只能跟着最多一个 remove() 操作,多次调用 remove() 操作会报错。
  • 迭代器类新增了一个 lastRet 成员变量,用来记录游标指向的前一个元素。通过迭代器去删除这个元素的时候,我们可以更新迭代器中的游标和 lastRet 值,来保证不会因为删除元素而导致某个元素遍历不到。
  • 如果通过容器来删除元素,并且希望更新迭代器中的游标值来保证遍历不出错,我们就要维护这个容器都创建了哪些迭代器,每个迭代器是否还在使用等信息,代码实现就变得比较复杂了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值