62、迭代器模式(中)

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

当通过迭代器来遍历集合的时候,增加、删除集合元素会导致不可预期的遍历结果
实际上 "不可预期" 比直接出错更加可怕,有的时候运行正确,有的时候运行错误,一些隐藏很深、很难 debug 的 bug 就是这么产生的

那我们如何才能避免出现这种不可预期的运行结果呢
有两种比较干脆利索的解决方案:一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错
第一种解决方案比较难实现,我们要确定遍历开始和结束的时间点

  • 遍历开始的时间节点我们很容易获得,我们可以把创建迭代器的时间点作为遍历开始的时间点
  • 但遍历结束的时间点该如何来确定呢
    你可能会说,遍历到最后一个元素的时候就算结束呗,但在实际的软件开发中,每次使用迭代器来遍历元素,并不一定非要把所有元素都遍历一遍
    你可能还会说,那我们可以在迭代器类中定义一个新的接口 finishIteration(),主动告知容器迭代器使用完了,你可以增删元素了
    但这就要求程序员在使用完迭代器之后要主动调用这个函数,也增加了开发成本,还很容易漏掉

第二种解决方法更加合理,Java 语言就是采用的这种解决方案,增删元素之后,让遍历报错,怎么确定在遍历时候集合有没有增删元素呢

  • 在 ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加 1
  • 当通过调用集合上的 iterator() 函数来创建迭代器的时候,我们把 modCount 值传递给迭代器的 expectedModCount 成员变量
  • 之后每次调用迭代器上的 hasNext()、next()、currentItem() 函数,我们都会检查集合上的 modCount 是否等于 expectedModCount
    也就是看,在创建完迭代器之后,modCount 是否改变过
    如果两个值不相同,那就说明集合存储的元素已经改变了,要么增加了元素,要么删除了元素
    之前创建的迭代器已经不能正确运行了,再继续使用就会产生不可预期的结果
    所以我们选择 fail-fast 解决方式,抛出运行时异常,结束掉程序,让程序员尽快修复这个因为不正确使用迭代器而产生的 bug
public class ArrayList<E> implements List<E> {

    // ...

    private java.util.ArrayList<E> list;
    public int modCount; // 修改次数

    public ArrayList() {
        list = new java.util.ArrayList<>();
        modCount = 0;
    }

    public void add(E e) {
        modCount++;
        list.add(e);
    }

    public E remove(int index) {
        modCount++;
        return list.remove(index);
    }

    public E get(int index) {
        return list.get(index);
    }

    public int size() {
        return list.size();
    }

    public Iterator<E> iterator() {
        return new ArrayIterator<>(this);
    }

    // 省略其他代码 ...
}
public class ArrayIterator<E> implements Iterator<E> {

    private int cursor; // 光标
    private ArrayList<E> arrayList;
    private int expectedModCount; // 预期修改数

    public ArrayIterator(ArrayList<E> arrayList) {
        this.cursor = 0;
        this.arrayList = arrayList;
        this.expectedModCount = arrayList.modCount;
    }

    @Override
    public boolean hasNext() {
        checkForCoModification();
        return cursor < arrayList.size(); // 注意这里, cursor 在指向最后一个元素的时候, hasNext() 仍旧返回 true
    }

    @Override
    public void next() {
        checkForCoModification();
        cursor++;
    }

    @Override
    public E currentItem() {
        checkForCoModification();
        if (cursor >= arrayList.size()) {
            throw new NoSuchElementException();
        }
        return arrayList.get(cursor);
    }

    /**
     * arrayList.modCount == this.expectedModCount
     * 检查在遍历的过程中是否增删元素了, 如果增删元素了, 就报错
     */
    private void checkForCoModification() {
        if (arrayList.modCount != expectedModCount) throw new ConcurrentModificationException();
    }
}

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

像 Java 语言,迭代器类中除了前面提到的几个最基本的方法之外,还定义了一个 remove() 方法,能够在遍历集合的同时,安全地删除集合中的元素
不过需要说明的是,它并没有提供添加元素的方法,毕竟迭代器的主要作用是遍历,添加元素放到迭代器里本身就不合适

我个人觉得 Java 迭代器中提供的 remove() 方法还是比较鸡肋的,作用有限
它只能删除游标指向的前一个元素,而且一个 next() 函数之后,只能跟着最多一个 remove() 操作,多次调用 remove() 操作会报错

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();
        iterator.remove();
        iterator.remove(); // 报错, 抛出 IllegalStateException 异常
    }
}

为什么通过迭代器就能安全的删除集合中的元素呢,我们来看下 remove() 函数是如何实现的,代码如下所示
稍微提醒一下,在 Java 实现中,迭代器类是容器类的内部类,并且 next() 函数不仅将游标后移一位,还会返回当前的元素

public class ArrayList<E> {

    transient Object[] elementData;
    private int size;

    public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {
        }

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

        @SuppressWarnings("unchecked")
        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];
        }

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

在上面的代码实现中,迭代器类新增了一个 lastRet 成员变量,用来记录游标指向的前一个元素
通过迭代器去删除这个元素的时候,我们可以更新迭代器中的游标和 lastRet 值,来保证不会因为删除元素而导致某个元素遍历不到
如果通过容器来删除元素,并且希望更新迭代器中的游标值来保证遍历不出错,我们就要维护这个容器都创建了哪些迭代器,每个迭代器是否还在使用等信息,代码实现就变得比较复杂了

posted @ 2023-07-09 12:03  lidongdongdong~  阅读(8)  评论(0编辑  收藏  举报