0
点赞
收藏
分享

微信扫一扫

JAVA设计模式之迭代器模式

小a草 2023-08-30 阅读 44

迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern)。

迭代器模式是一种行为型设计模式,它用于遍历集合对象,而无需暴露该对象的底层表示。这种模式非常适合在处理大型集合时使用,因为它提供了一种更抽象的方式来访问集合的元素

迭代器模式的几个关键组件:

  1. Iterator接口:定义了遍历集合所需的操作,例如next()、hasNext()等。
  2. ConcreteIterator类:实现Iterator接口,具体实现遍历的细节。
  3. Aggregate接口:定义创建Iterator对象的方法。
  4. ConcreteAggregate类:实现Aggregate接口,具体实现创建ConcreteIterator的方法。

创建一个基于迭代器模式的整数数组遍历示例。需要创建相应的接口和类。在这个例子中,将创建一个IntArray类,它实现了Aggregate接口,并使用IntArrayIterator类作为具体迭代器。

1、Iterator接口:

public interface Iterator {
    boolean hasNext();
    Object next();
}

2、创建具体的迭代器类(IntArrayIterator):

public class IntArrayIterator implements Iterator {
    private IntArray intArray;
    private int index;

    public IntArrayIterator(IntArray intArray) {
        this.intArray = intArray;
        this.index = 0;
    }

    @Override
    public boolean hasNext() {
        return index < intArray.getLength();
    }

    @Override
    public Object next() {
        int value = intArray.getValueAt(index);
        index++;
        return value;
    }
}

3、Aggregate接口:

public interface Aggregate {
    Iterator iterator();
}

4、创建具体的聚合类(IntArray):

public class IntArray implements Aggregate {
    private int[] values;

    public IntArray(int[] values) {
        this.values = values;
    }

    public int getValueAt(int index) {
        return values[index];
    }

    public int getLength() {
        return values.length;
    }

    @Override
    public Iterator iterator() {
        return new IntArrayIterator(this);
    }
}

可以使用迭代器模式来遍历一个整数数组:

public class Main {
    public static void main(String[] args) {
        int[] values = {1, 2, 3, 4, 5};
        IntArray intArray = new IntArray(values);

        Iterator iterator = intArray.iterator();
        while (iterator.hasNext()) {
            int value = (int) iterator.next();
            System.out.println(value);
        }
    }
}

运行这个程序,将看到数组中的整数按顺序输出。这就是一个简单的基于迭代器模式的整数数组遍历示例。

jdk中的迭代器

使用

在Java中,许多集合类(如ArrayList)已经实现了内置的迭代器。下面是一个使用迭代器遍历ArrayList的示例,以及相应的中文注释:

public class Main {
    public static void main(String[] args) {
        // 创建一个ArrayList对象
        ArrayList<String> names = new ArrayList<>();

        // 向ArrayList中添加元素
        names.add("张三");
        names.add("李四");
        names.add("王五");

        // 获取ArrayList的迭代器对象
        Iterator<String> iterator = names.iterator();

        // 使用迭代器遍历ArrayList
        while (iterator.hasNext()) {
            // 使用next()方法获取下一个元素
            String name = iterator.next();
            System.out.println(name);
        }
    }
}

首先创建了一个ArrayList对象并添加了一些字符串。通过调用names.iterator()方法获得了一个迭代器对象。接下来,使用hasNext()next()方法遍历ArrayList中的元素。

源码

在jdk提供中,迭代器接口位于java.util.Iterator,他有很多具体的实现,选取日常工作中最常见的ArrayList为例其继承结构如下:

JAVA设计模式之迭代器模式_迭代器

在AbstractList抽象类中,定义了如下的内部类,只看主干方法:

1、hasNext()方法的逻辑是,资源游标未到之后一个就认为其有下一个。

2、next()方法的处理逻辑是:调用子类的get方法(模板方法设计模式),并让游标后移一位。

3、remove()方法的逻辑是:调用子类的remove方法。

在这三个核心方法中先不去看中间的一些成员变量,主干内容很好理解:

private class Itr implements Iterator<E> {
    /**
         * Index of element to be returned by subsequent call to next.
         */
    int cursor = 0;

    /**
         * Index of element returned by most recent call to next or
         * previous.  Reset to -1 if this element is deleted by a call
         * to remove.
         */
    int lastRet = -1;

    /**
         * The modCount value that the iterator believes that the backing
         * List should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
    int expectedModCount = modCount;

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

    public E next() {
        checkForComodification();
        try {
            int i = cursor;
            E next = get(i);
            lastRet = i;
            cursor = i + 1;
            return next;
        } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException(e);
        }
    }

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

        try {
            AbstractList.this.remove(lastRet);
            if (lastRet < cursor)
                cursor--;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException e) {
            throw new ConcurrentModificationException();
        }
    }

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

事实上,抽象的父类提供了公用的方法:

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

可以使用该共享方法为其子类创建迭代器。

迭代时增删元素

在使用迭代器迭代一个集合时,使用集合原生的方法进行删除,会发生什么呢?

public class Main {
    public static void main(String[] args) {
        // 创建一个ArrayList对象
        ArrayList<String> names = new ArrayList<>();

        // 向ArrayList中添加元素
        names.add("张三");
        names.add("李四");
        names.add("王五");

        // 获取ArrayList的迭代器对象
        Iterator<String> iterator = names.iterator();

        // 使用迭代器遍历ArrayList
        while (iterator.hasNext()) {
            // 使用next()方法获取下一个元素
            String name = iterator.next();
            names.remove(name);
            System.out.println(name);
        }
    }
}

尝试了,会发现会发生如下的异常;

JAVA设计模式之迭代器模式_System_02

q: 遍历集合的同时,为什么不能增删集合元素?

道理很简单,在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素时,有可能会导致某个元素被重复遍历或遍历不到。不过,并不是所有情况下都会遍历出错,有的时候也可以正常遍历,所以,这种行为称为结果不可预期行为或者未决行为

事实上抛出异常,也是因为实现的迭代器中做了特殊处理。编写的简单迭代器并不会抛出异常,只是因为并没有做特殊处理。

**注:**要知道不抛出异常不代表可以正常遍历,抛出异常只是更加友好的提示。

看一下这个题目:我想把下边的集合中的lucy全部删除?

public void add() {
    List<String> names = new ArrayList<>();
    names.add("tom");
    names.add("lucy");
    names.add("lucy");
    names.add("lucy");
    names.add("jerry");
}

(1)for循环

首先想到的就是以下使用for循环,进行判断和删除:

public void testDelByFor(){
    for (int i = 0; i < names.size(); i++) {
        if("lucy".equals(names.get(i))){
            names.remove(i);
        }
    }
    System.out.println(names);
}

结果:
[tom, lucy, jerry]

发现并没有删除干净,中间的lucy好像被遗忘了。

JAVA设计模式之迭代器模式_迭代器_03

合适的解决方式有两种:

第一种:回调指针

for (int i = 0; i < names.size(); i++) {
    if("lucy".equals(names.get(i))){
        names.remove(i);
        // 回调指针:
        i--;
    }
}
System.out.println(names);


结果:
[tom, jerry]

第二种:逆序遍历

for (int i = names.size()-1; i > 0; i--) {
    if("lucy".equals(names.get(i))){
        names.remove(i);
    }
}
System.out.println(names);

结果:
[tom, jerry]

结合指针移动和链表的位置移动过程,为什么这两种方案,可以解决遍历中删除的问题。

(3)使用迭代器删除元素

事实上,使用迭代器的api进行删除是没有问题的:

public static void main(String[] args){
    Iterator<String> iterator = names.iterator();
    while (iterator.hasNext()){
        // 记住next(),只能调用一次,因为每次调用都会选择下一个
        String name = iterator.next();
        if("lucy".equals(name)){
            iterator.remove();
        }
    }
    System.out.println(names);
}

根本原因是迭代器的remove使用的指针回拨解决了这个问题。

并发迭代时删除

上边代码中看到的异常,也叫并发修改异常,也就意味着这个异常的产生本质上应该是在并发修改一个集合时产生的,将代码改为如下的内容:

public static void main(String[] args) {
    // 创建一个ArrayList对象
    ArrayList<String> names = new ArrayList<>();

    // 向ArrayList中添加元素
    names.add("张三");
    names.add("李四");
    names.add("王五");

    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            Iterator<String> iterator = names.iterator();
            while (iterator.hasNext()){
                // 记住next(),只能调用一次,因为每次调用都会选择下一个
                String name = iterator.next();
                if("李四".equals(name)){
                    iterator.remove();
                }
                System.out.println(names);
            }
        }).start();
    }
}

确实再次看到了这个异常:

JAVA设计模式之迭代器模式_迭代器_04

可以想一下,正在迭代一个集合,此时有人却修改了这个集合,为了防止这样的事情发生引入了这个异常,可以并发读,单不允许并发改。

在 ArrayList 中定义一个成员变量 modCount,记录集合被修改的次数,集合每调用一次增加或删除元素的函数,就会给 modCount 加 1。

当通过调用集合上的 iterator() 函数来创建迭代器的时候,把 modCount 值传递给迭代器的 expectedModCount 成员变量,之后每次调用迭代器上的 hasNext()、next()、currentItem() 函数,都会检查集合上的 modCount 是否等于 expectedModCount,也就是看,在创建完迭代器之后,modCount 是否改变过。

如果两个值不相同,那就说明集合存储的元素已经改变了,要么增加了元素,要么删除了元素,之前创建的迭代器已经不能正确运行了,再继续使用就会产生不可预期的结果,所以选择 fail-fast 解决方式,抛出运行时异常,结束掉程序,让程序员尽快修复这个因为不正确使用迭代器而产生的 bug。

利用快照解决并发问题

在这个示例中,为迭代器添加了 remove() 方法,允许在迭代过程中删除元素。注意,这里的删除操作会影响原始数据结构,但不会影响快照。

(1)修改迭代器接口,添加 remove() 方法:

public interface SnapshotIterator {
    boolean hasNext();
    Integer next();
    void remove();
}

(2)修改支持快照功能的迭代器类,每次构建迭代器,需要将数据拷贝,实现 remove() 方法:

public class SnapshotIteratorImpl implements SnapshotIterator {
    private DataStructure dataStructure;
    private List<Integer> snapshot;
    private int index;

    // 构造方法,创建一个当前数据结构的快照
    public SnapshotIteratorImpl(DataStructure dataStructure) {
        this.dataStructure = dataStructure;
        this.snapshot = new ArrayList<>(dataStructure.size());
        for (int i = 0; i < dataStructure.size(); i++) {
            snapshot.add(dataStructure.get(i));
        }
        this.index = 0;
    }

    // 判断是否有下一个元素
    @Override
    public boolean hasNext() {
        return index < snapshot.size();
    }

    // 获取下一个元素
    @Override
    public Integer next() {
        if (hasNext()) {
            return snapshot.get(index++);
        }
        return null;
    }

    // 删除当前元素
    @Override
    public void remove() {
        if (index > 0) {
            Integer itemToRemove = snapshot.get(index--);
            dataStructure.removeItem(itemToRemove);
        }
    }
}

(2)修改 DataStructure 类,添加 removeItem() 方法:

// 自定义一个数据结构类
public class DataStructure {
    private List<Integer> items;

    public DataStructure(List<Integer> items) {
        this.items = new ArrayList<>(items);
    }

    public Integer get(int index) {
        return items.get(index);
    }

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

    public void removeItem(Integer item) {
        items.remove(item);
    }
}

(3)使用示例:

public class Main {
    public static void main(String[] args) {
        List<Integer> items = Arrays.asList(1, 2, 3, 4, 5);
        DataStructure dataStructure = new DataStructure(items);

        SnapshotIterator iterator = new SnapshotIteratorImpl(dataStructure);
        while (iterator.hasNext()) {
            Integer item = iterator.next();
            System.out.println(item);

            if (item % 2 == 0) {
                iterator.remove(); // 删除偶数
            }
        }

        System.out.println("After removal:");
        SnapshotIterator iterator2 = new SnapshotIteratorImpl(dataStructure);
        while (iterator2.hasNext()) {
            System.out.println(iterator2.next());
        }
    }
}

在 SnapshotIterator 接口中添加了 remove() 方法,同时在 SnapshotIteratorImpl 类中实现了该方法。remove() 方法会从原始数据结构中删除元素,但不会影响快照。还为 DataStructure 类添加了一个 removeItem() 方法以支持删除操作。在主函数中,演示了在演示示例中,迭代数据结构并删除所有偶数元素。创建一个新的迭代器,展示删除操作后的数据结构。

public class Main {
    public static void main(String[] args) {
        List<Integer> items = Arrays.asList(1, 2, 3, 4, 5);
        DataStructure dataStructure = new DataStructure(items);

        SnapshotIterator iterator = new SnapshotIteratorImpl(dataStructure);
        System.out.println("Original list:");
        while (iterator.hasNext()) {
            Integer item = iterator.next();
            System.out.println(item);

            if (item % 2 == 0) {
                iterator.remove(); // 删除偶数
            }
        }

        System.out.println("After removal:");
        SnapshotIterator iterator2 = new SnapshotIteratorImpl(dataStructure);
        while (iterator2.hasNext()) {
            System.out.println(iterator2.next());
        }
    }
}

运行这个程序,将看到以下输出:

Original list:
After removal:

如上所示,程序首先迭代原始列表并打印元素。在迭代过程中,删除了所有偶数元素。然后创建了一个新的迭代器并打印出删除操作后的数据结构。可以看到,删除操作已成功删除了偶数元素。

这个实现使得迭代器可以在迭代过程中修改原始数据结构,同时保持快照功能。

事实上,以上的实现在多线程环境下可能会遇到线程安全问题。如果有多个线程同时访问和修改原始数据结构(DataStructure 类的实例),可能会导致不可预期的行为和数据不一致。为了确保线程安全,可以采用以下方法之一:

(1)使用同步关键字(synchronized):

在 DataStructure 类的方法中,添加 synchronized 关键字以确保在同一时间只有一个线程可以访问这些方法。这样可以保证在多线程环境下的线程安全性。

public class DataStructure {
    // ...

    public synchronized Integer get(int index) {
        return items.get(index);
    }

    public synchronized int size() {
        return items.size();
    }

    public synchronized void removeItem(Integer item) {
        items.remove(item);
    }
}

(2)使用 java.util.concurrent.locks 中的锁(如 ReentrantLock):

另一种确保线程安全的方法是使用显式锁。在这个例子中,可以使用 ReentrantLock。这种方法的优点是可以实现更细粒度的锁定控制,但需要手动管理锁的获取和释放。

public class DataStructure {
    private List<Integer> items;
    private final Lock lock = new ReentrantLock();

    // ...

    public Integer get(int index) {
        lock.lock();
        try {
            return items.get(index);
        } finally {
            lock.unlock();
        }
    }

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

    public void removeItem(Integer item) {
        lock.lock();
        try {
            items.remove(item);
        } finally {
            lock.unlock();
        }
    }
}

这两种方法在不同程度上牺牲了性能。同步方法或锁机制可能导致线程阻塞,从而降低程序的执行速度。在选择合适的线程安全策略时,请根据具体需求和性能要求进行权衡。

优化

为每个元素保存两个时间戳,一个是添加时间戳 addTimestamp,一个是删除时间戳 delTimestamp。当元素被加入到集合中的时候,将 addTimestamp 设置为当前时间,将 delTimestamp 设置成最大长整型值(Long.MAX_VALUE)。当元素被删除时,将 delTimestamp 更新为当前时间,表示已经被删除。同时,每个迭代器也保存一个迭代器创建时间戳 snapshotTimestamp,也就是迭代器对应的快照的创建时间戳。当使用迭代器来遍历容器的时候,只有满足 addTimestamp<snapshotTimestamp<delTimestamp 的元素,才是属于这个迭代器的快照。 如果元素的 addTimestamp>snapshotTimestamp,说明元素在创建了迭代器之后才加入的,不属于这个迭代器的快照;如果元素的 delTimestamp<snapshotTimestamp,说明元素在创建迭代器之前就被删除掉了,也不属于这个迭代器的快照。

通过为每个元素和迭代器添加时间戳,可以在遍历过程中准确地识别哪些元素属于迭代器的快照。以下是基于这个思路的 Java 实现:

(1)定义一个包含元素和时间戳的类 TimestampedItem

public class TimestampedItem<T> {
    private T item;
    private long addTimestamp;
    private long delTimestamp;

    public TimestampedItem(T item) {
        this.item = item;
        this.addTimestamp = System.currentTimeMillis();
        this.delTimestamp = Long.MAX_VALUE;
    }

    public T getItem() {
        return item;
    }

    public long getAddTimestamp() {
        return addTimestamp;
    }

    public long getDelTimestamp() {
        return delTimestamp;
    }

    public void markAsDeleted() {
        this.delTimestamp = System.currentTimeMillis();
    }
}

(2)修改 DataStructure 类,使其包含 TimestampedItem

public class DataStructure<T> {
    private List<TimestampedItem<T>> items;

    public DataStructure(List<T> items) {
        this.items = new ArrayList<>(items.size());
        for (T item : items) {
            this.items.add(new TimestampedItem<>(item));
        }
    }

    public TimestampedItem<T> get(int index) {
        return items.get(index);
    }

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

    public void removeItem(T item) {
        for (TimestampedItem<T> timestampedItem : items) {
            if (timestampedItem.getItem().equals(item)) {
                timestampedItem.markAsDeleted();
                break;
            }
        }
    }
}

(3)修改迭代器接口和实现类,使其适应 TimestampedItem 类型的元素,并添加快照时间戳:

public interface SnapshotIterator<T> {
    boolean hasNext();
    T next();
    void remove();
}

public class SnapshotIteratorImpl<T> implements SnapshotIterator<T> {
    private DataStructure<T> dataStructure;
    private int index;
    private long snapshotTimestamp;

    public SnapshotIteratorImpl(DataStructure<T> dataStructure) {
        this.dataStructure = dataStructure;
        this.index = 0;
        this.snapshotTimestamp = System.currentTimeMillis();
    }

    @Override
    public boolean hasNext() {
        // 这个过程会查找时间戳
        while (index < dataStructure.size()) {
            TimestampedItem<T> currentItem = dataStructure.get(index);
            if (currentItem.getAddTimestamp() < snapshotTimestamp &&
                currentItem.getDelTimestamp() > snapshotTimestamp) {
                return true;
            }
            index++;
        }
        return false;
    }

    @Override
    public T next() {
        if (hasNext()) {
            return dataStructure.get(index++).getItem();
        }
        return null;
    }

    @Override
    public void remove() {
        if (index > 0) {
            int currentIndex = index - 1;
            T itemToRemove = dataStructure.get(currentIndex).getItem();
            dataStructure.removeItem(itemToRemove);
        }
    }
}

(4)使用示例:

public class Main {
    public static void main(String[] args) {
        List<Integer> items = Arrays.asList(1, 2, 3, 4, 5);
        DataStructure<Integer> dataStructure = new DataStructure<>(items);
        SnapshotIterator<Integer> iterator = new SnapshotIteratorImpl<>(dataStructure);
        System.out.println("Original list:");
        while (iterator.hasNext()) {
            Integer item = iterator.next();
            System.out.println(item);

            if (item % 2 == 0) {
                iterator.remove(); // 删除偶数
            }
        }

        System.out.println("After removal:");
        SnapshotIterator<Integer> iterator2 = new SnapshotIteratorImpl<>(dataStructure);
        while (iterator2.hasNext()) {
            System.out.println(iterator2.next());
        }
    }
}

对于时间戳来说,Java中的System.currentTimeMillis()方法在多线程环境下是线程安全的。然而,仅仅依赖时间戳来处理并发问题还是不够的。考虑以下场景:

  1. 当两个线程同时尝试删除同一个元素时,可能会出现竞态条件。虽然它们都会尝试将delTimestamp更新为当前时间,但这个过程不是原子性的,可能导致不一致的结果。
  2. 当一个线程在遍历数据结构时,另一个线程可能正在修改数据结构(例如,添加或删除元素)。这种情况下,依赖于时间戳的迭代器可能无法确保数据的一致性。在某些情况下,这可能导致不稳定的迭代器行为。

因此,尽管时间戳本身是线程安全的,但在实际操作中,还需要额外的同步机制来确保整个过程的线程安全。根据需求和性能要求,可以使用前面提到的方法,例如同步关键字(synchronized)或显式锁(如ReentrantLock)。

时间戳在这个实现中的主要目的是跟踪元素的添加和删除状态,从而在迭代器中准确地识别哪些元素属于迭代器的快照。时间戳可以在不复制整个数据结构的情况下,实现一种“轻量级”的快照功能。

以下是时间戳在这个实现中的主要用途:

  1. 记录元素的添加时间:addTimestamp 用于保存元素添加到数据结构的时间。这样,在创建迭代器时,可以根据元素的添加时间判断它是否属于快照。
  2. 记录元素的删除时间:delTimestamp 用于保存元素从数据结构中删除的时间。使用迭代器遍历数据结构时,可以根据元素的删除时间判断它是否仍属于快照。
  3. 记录迭代器快照创建时间:snapshotTimestamp 用于保存迭代器创建时的时间戳。通过比较元素的 addTimestamp 和 delTimestamp 与迭代器的 snapshotTimestamp,可以判断元素是否属于迭代器的快照。

虽然时间戳在这个场景中提供了一种有效的方法来实现迭代器的快照功能,但它并不能解决多线程环境下的线程安全问题。为了确保线程安全,需要使用额外的同步机制(如前面提到的同步关键字或显式锁)。总的来说,时间戳在这里主要用于实现快照功能,而非解决线程安全问题。

举报

相关推荐

0 条评论