设计模式-迭代器模式

设计模式-迭代器模式

迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern),用来给类实例提供一种遍历对象的方式。

案例分析

首先写一个经典的 User 类

java 复制代码
@Data
@ToString
public class User {

    private String uuid;

    private String name;

    private Integer age;

}

通常我们遍历一个对象有三种方式

java 复制代码
public class Main {
    public static void main(String[] args) {

        List<User> userList = IntStream.rangeClosed(1, 10).mapToObj(i -> {
            User user = new User();
            user.setUuid(i + "");
            user.setAge(i);
            user.setName("name" + i);
            return user;
        }).collect(Collectors.toList());

        for (int i = 0; i < userList.size(); i++) {
            User temp = userList.get(i);
            System.out.println(temp);
        }

        for (User temp : userList) {
            System.out.println(temp);
        }

        Iterator<User> iterator = userList.iterator();
        while (iterator.hasNext()) {
            User temp = iterator.next();
            System.out.println(temp);
        }

    }
}

不过如果分析上述这段代码编译出的字节码文件,其实方法二和方法三最终的实现是一样的

java 复制代码
        for (User temp : userList) {
            System.out.println(temp);
        }

        Iterator<User> iterator = userList.iterator();
        while (iterator.hasNext()) {
            User temp = iterator.next();
            System.out.println(temp);
        }

实现一个迭代器其实并不复杂,我们可能会好奇一个问题,为什么 userList 可以放在增强 for 循环的位置

java 复制代码
        for (User temp : userList) {
            System.out.println(temp);
        }

而 User 对象本身不能放在增强 for 循环的位置

java 复制代码
        for (String temp : User) {
            System.out.println(temp);
        }

结合标题迭代器模式我们很容易想到这个因为 userList 对应的 ArrayList 类实现了迭代器接口

java 复制代码
public interface Iterable<T> {
    /**
     * Returns an iterator over elements of type {@code T}.
     *
     * @return an Iterator.
     */
    Iterator<T> iterator();
}    

所以如果想让 User 类也可以放到增强 for 循环的位置,也需要实现这个接口

java 复制代码
public class User implements Iterable<String>{

    private String uuid;

    private String name;

    private Integer age;

    @Override
    public Iterator<String> iterator() {
        return new UserIte();
    }
    
    private class UserIte implements Iterator<String> {

        @Override
        public boolean hasNext() {
            return false;
        }

        @Override
        public String next() {
            return null;
        }
    }
    
}

所以实现一个迭代器类只需要两步:

  • 类实现 Iterable,代表是可迭代的
  • 在类中创建一个内部类实现迭代器接口,实现其中的是否还有下一个元素接口和获取下一个元素接口

由于我们需要记录是否遍历完成,所以 UserIte 中需要使用属性记录当前遍历到的位置

java 复制代码
@Data
@ToString
public class User implements Iterable<String> {

    private String uuid;

    private String name;

    private Integer age;

    @Override
    public Iterator<String> iterator() {
        return new UserIte();
    }

    private class UserIte implements Iterator<String> {

        private Integer index;

        private List<String> propertyList = new ArrayList<>();

        public UserIte() {
            index = 0;
            propertyList.add(User.this.uuid);
            propertyList.add(User.this.name);
            propertyList.add(User.this.age + "");
        }

        @Override
        public boolean hasNext() {
            return index < propertyList.size();
        }

        @Override
        public String next() {
            if (index >= propertyList.size()) {
                throw new NoSuchElementException("没有" + index + "对应的元素");
            }
            return propertyList.get(index++);
        }
    }

}

这样就可以对 User 的对象进行遍历了

java 复制代码
public class Main {
    public static void main(String[] args) {
        List<User> userList = IntStream.rangeClosed(1, 10).mapToObj(i -> {
            User user = new User();
            user.setUuid(i + "");
            user.setAge(i);
            user.setName("name" + i);
            return user;
        }).collect(Collectors.toList());

        User temp = userList.get(0);
        for (String s : temp) {
            System.out.println(s);
        }
    }
}

实现一个迭代器并不是很复杂,但是如果你仔细看上面的实现就会有疑问,如果在遍历的过程中对元素进行了变化,那么迭代过程中会出现什么问题呢?

这里不再演示之间说结论,有可能出错也有可能不出错,迭代器中有一个 cursor 的概念,Mysql 中其实也有这个游标的概念。

如上图所示:当游标指向 b 的时候,删除了数组中的 a 元素,由于数组会进行补齐,所以 cursor 会指向 d,元素 c 就会错过遍历。如果指向最后一个元素的时候被删了,那么可能会出现空指针。事实上,在计算机中,不可预知的结果(出错或不出错)比出错更让人头疼,因此最好的处理方式就是只要出现了预期之外的操作就报错,让开发人员尽快处理这样的操作。

如果你查看 ArrayList 的实现,会发现其在每次进行 add 或 remove 操作的时候,会对其中的 modCount 进行 +1,记录出现的更新次数,并在创建迭代器时将该次数传入,并在每次进行遍历时先判断迭代器中修改次数的和实例的修改次数是否相同,如果不同就抛出异常。

总结

所以,实现一个迭代器模式并不复杂,但是迭代器存在的意义是什么呢?为什么不都用 for i 类型的遍历呢?

对于复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。比如,树有前中后序、按层遍历,图有深度优先、广度优先遍历等等。如果由客户端代码来实现这些遍历算法,势必增加开发成本,而且容易写错。如果将这部分遍历的逻辑写到容器类中,也会导致容器类代码的复杂性。

前面也多次提到,应对复杂性的方法就是拆分。我们可以将遍历操作拆分到迭代器类中。比如,针对图的遍历,我们就可以定义 DFSIterator、BFSIterator 两个迭代器类,让它们分别来实现深度优先遍历和广度优先遍历。

其次,将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。这样,我们就可以创建多个不同的迭代器,同时对同一个容器进行遍历而互不影响。而且迭代器之间都实现了迭代器接口,可以利用多态特性很方便的互相替换,例如从前遍历转为从后遍历,体现了面向接口编程的思想。

相关推荐
weixin_4487717211 分钟前
Spring框架之ApplicationContext源码学习
java·学习·spring
白露与泡影19 分钟前
Spring AI Alibaba -- 文生图、文生音及向量化
java·人工智能·spring
滕州市燕猫虎计算机科技工作室个体工商户19 分钟前
Java,Java NIO,Java NIO2,Java AIO,AsynchronousServerSocketChannel
java·开发语言·nio·aio·nio2
学长毕设28 分钟前
基于springboot的社会志愿者管理系统-附源码
java·spring boot·后端
木头科技1 小时前
【AI 工程化第十六篇】Spring AI 多模态 RAG 实战:图片、扫描件、表格与图文证据链
java·人工智能·spring
驭渊的小故事1 小时前
从 JVM 底层看泛型:泛型擦除、Signature 属性与 Super Type Token 模式
java·开发语言·jvm
菜鸟小杰子1 小时前
手动修改 Spring Boot 嵌套 JAR 包:大包小包隔离打包实战
java·jar
计算机编程-吉哥1 小时前
计算机毕业设计 基于 SpringBoot + Vue 的剧本杀系统 Java毕业设计【任务书、开题报告、中期报告、论文、源码】
java·vue.js·spring boot·毕业设计·课程设计·计算机毕业设计选题
工一木子1 小时前
设计模式与生活③:你的早晨,是一套模板
设计模式
code2cat1 小时前
【随笔】RAG语义缓存:问题很相似,答案就能直接复用吗
java·缓存·知识库·rag