设计模式-迭代器模式

2023-11-15

迭代器模式

1、迭代器模式介绍

迭代器模式是一种行为型设计模式,它提供了一种方法来访问聚合对象中的各个元素,而不暴露其内部表示。通过使用迭代器,客户端可以遍历一个聚合对象中的元素,而不必了解其内部实现。

在迭代器模式中,定义了一个迭代器接口,该接口声明了能够访问聚合对象中元素的方法。然后,具体的迭代器实现类实现了这个接口,并提供了一个具体的遍历算法。聚合对象则负责创建并返回其对应的迭代器实例。

例如:在以前坐公交得自己交钱,售票员根据上车的人进行售票,不管什么人都要买票(比如小偷、公交车公司内部人员、程序员等同样要买票人人平等)。售票员其实就是将所有人都遍历了一遍,对每个乘客都要买票。

1.1 迭代器模式基本实现

迭代器模式(Iterator),提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。

迭代器模式结构图:

Aggregate聚集抽象类:

 

java

复制代码

/** * @author Shier * CreateTime 2023/5/13 21:07 * 聚集抽象类 */ public abstract class Aggregate { /** * 创建迭代器 * * @return */ public abstract Iterator createIterator(); }

ConcreteAggregate具体聚集类:继承Aggregate。

 

java

复制代码

/** * @author Shier * CreateTime 2023/5/13 21:09 * 具体聚集类 */ public class ConcreteAggregate extends Aggregate { /** * 存储聚合对象 */ private List<Object> list = new ArrayList<>(); @Override public Iterator createIterator() { return (Iterator) new ConcreteIterator(this); } /** * 返回聚合总个数 * * @return */ public int getCount() { return list.size(); } /** * 增加新对象 * * @param obj */ public void add(Object obj) { list.add(obj); } /** * 得到指定的对象 */ public Object getCurrentItem(int index) { return list.get(index); } }

Iterator迭代器抽象类:

 

java

复制代码

/** * @author Shier * CreateTime 2023/5/13 21:16 * Iterator 迭代器对象类 */ public abstract class Iterator { // 第一个 public abstract Object first(); // 下一个 public abstract Object netx(); // 是否到最后 public abstract boolean isDone(); // 当前对象 public abstract Object currentItems(); }

ConcreteIterator具体迭代器类:继承Iterator。

 

java

复制代码

/** * @author Shier * CreateTime 2023/5/13 21:15 */ public class ConcreteIterator extends Iterator { private ConcreteAggregate aggregate; public ConcreteIterator(ConcreteAggregate aggregate) { this.aggregate = aggregate; } private int current = 0; /** * 得到第一个对象 * * @return */ @Override public Object first() { return aggregate.getCurrentItem(0); } /** * 得到下一个对象 * * @return */ @Override public Object netx() { Object ret = null; current++; if (current < aggregate.getCount()) { ret = aggregate.getCurrentItem(current); } return ret; } /** * 判断是否到结尾 * * @return */ @Override public boolean isDone() { return current >= aggregate.getCount() ? true : false; } /** * 返回当前对象 * * @return */ @Override public Object currentItems() { return aggregate.getCurrentItem(current); } }

客户端代码:

 

java

复制代码

/** * @author Shier * CreateTime 2023/5/13 21:27 */ public class BaseClient { public static void main(String[] args) { // 聚集对象 上面的例子就相当于公交车bus ConcreteAggregate aggregateBus = new ConcreteAggregate(); aggregateBus.add("shier"); aggregateBus.add("公交公司员工"); aggregateBus.add("小菜"); aggregateBus.add("大白"); aggregateBus.add("小黑"); aggregateBus.add("小偷"); // 迭代器对象声明,即相当于售票员 ConcreteIterator conductor = new ConcreteIterator(aggregateBus); // 向第一个乘客售票 conductor.first(); while (!conductor.isDone()) { // 没有到最后一个则一直走向下一个乘客 System.out.println(conductor.currentItems() + ": 请买票!"); conductor.netx(); } } }

输出结果:

同时还可以实现倒序:

 

java

复制代码

/** * @author Shier * CreateTime 2023/5/13 21:15 * 具体迭代器类 - 倒序 */ public class ConcreteIteratorDesc extends Iterator { private ConcreteAggregate aggregate; private int current = 0; public ConcreteIteratorDesc(ConcreteAggregate aggregate) { this.aggregate = aggregate; // 从最后一个开始 current = aggregate.getCount() - 1; } /** * 得到倒数第一个对象 * * @return */ @Override public Object first() { return aggregate.getCurrentItem(aggregate.getCount() - 1); } /** * 得到下一个对象 * * @return */ @Override public Object netx() { Object ret = null; // 递减 current--; if (current < aggregate.getCount()) { ret = aggregate.getCurrentItem(current); } return ret; } /** * 判断是否到结尾 * * @return */ @Override public boolean isDone() { return current >= aggregate.getCount() ? true : false; } /** * 返回当前对象 * * @return */ @Override public Object currentItems() { return aggregate.getCurrentItem(current); } }

你想呀,售票员才不管你上来的是人还是物(行李),不管是中国人还 是外国人,不管是不是内部员工,甚至哪怕是马上要抓走的小偷,只要是来 乘车的乘客,就必须要买票。同样道理,当你需要访问一个聚集对象,而且 不管这些对象是什么都需要遍历的时候,你就应该考虑用迭代器模式。对聚集有多种方式遍历时,可以考虑用迭代器模式。售票员从车头到车尾来售票,也可以从车尾向车头来售票,也就是说, 你需要对聚集有多种方式遍历时,可以考虑用迭代器模式。由于不管乘客是 什么,售票员的做法始终是相同的,都是从第一个开始,下一个是谁,是否 结束,当前售到哪个人了,这些方法每天他都在做,也就是说,为遍历不同的聚集结构提供如开始、下一个、是否结束、当前哪一项等统一的接口

2、具体例子说明

2.1 购物车案例

上面也是举例一个坐公交车的列子

下面在用一个例子说明:

  1. 假设有一个超市的购物车,其中包含了多种商品,比如苹果、橙子、香蕉等。这个购物车可以看作是一个聚合对象,而其中每种商品则可以看作是购物车中的元素。

如果要遍历购物车中的所有商品,可以使用迭代器模式来实现。首先,定义一个迭代器接口,声明能够访问购物车中商品的方法。比如,可以定义一个Iterator接口,其中包含hasNext()next()两个方法。

  1. 然后,具体的迭代器实现类可以实现这个接口,并提供一个具体的遍历算法。例如,可以定义一个CartIterator类,它维护了购物车的内部状态,并实现了hasNext()next()方法,通过这两个方法来依次返回购物车中的每个商品。

  2. 最后,购物车可以负责创建并返回其对应的迭代器实例。可以定义一个Cart类,其中包含了购物车中所有商品的列表,以及一个getIterator()方法,用于返回一个CartIterator实例。

这样,客户端就可以通过调用getIterator()方法来获取购物车对应的迭代器,并使用它来遍历购物车中的所有商品。这种实现方式让遍历算法与聚合对象分离开来,使代码更加灵活和易于维护。

首先,定义迭代器接口 CartIterator

 

java

复制代码

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

然后,定义具体的迭代器实现类 CartIteratorImpl

 

java

复制代码

public class CartIteratorImpl implements CartIterator { private List<Object> cartList; // 购物车中的商品列表 private int position; // 迭代器当前位置 public CartIteratorImpl(List<Object> cartList) { this.cartList = cartList; this.position = 0; } @Override public boolean hasNext() { return position < cartList.size(); } @Override public Object next() { if (!hasNext()) { throw new NoSuchElementException(); } Object item = cartList.get(position); position++; return item; } }

CartIteratorImpl 类中,我们使用了一个 List<Object> 来保存购物车中的商品列表,在 hasNext()next() 方法中维护了内部状态 position,以便顺序地遍历购物车中的每个商品。

最后,定义聚合对象 Cart,并在其中实现 getIterator() 方法:

 

java

复制代码

public class Cart { private List<Object> itemList; public Cart() { this.itemList = new ArrayList<>(); } public void addItem(Object item) { itemList.add(item); } public void removeItem(Object item) { itemList.remove(item); } public CartIterator getIterator() { return new CartIteratorImpl(itemList); } }

Cart 类中,我们使用了一个 List<Object> 来保存购物车中的商品列表,实现了添加和删除商品的方法。同时,实现了 getIterator() 方法,用于返回购物车对应的迭代器实例 CartIteratorImpl

最后,可以在客户端代码中使用 CartCartIterator 来遍历购物车中的所有商品:

 

java

复制代码

public class Client { public static void main(String[] args) { Cart cart = new Cart(); cart.addItem("苹果"); cart.addItem("橙子"); cart.addItem("香蕉"); CartIterator iterator = cart.getIterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } } }

输出结果:

 

复制代码

苹果 橙子 香蕉

2.2 Java迭代器实现

起始在开发过程中不会向上面那样去搞,因为目前很多开发语言以及接入了这个迭代器模式,比如在Java中以及有了一个Iterator类

Java 中的 Iterator 类是迭代器模式的一种典型实现。它提供了一种统一的遍历方式,可以对不同类型的集合进行遍历,而无需关心集合内部的实现细节。

在 Java 中,每个实现了 Iterable 接口的对象都可以返回一个 Iterator 对象,用于遍历它所包含的元素。Iterator 接口定义了三个方法:

  • boolean hasNext():判断集合中是否还有下一个元素;
  • E next():返回集合中的下一个元素;
  • void remove():在迭代过程中移除集合中的当前元素,可选操作。

通过调用 hasNext()next() 方法,可以依次访问集合中的每个元素,直到遍历完所有元素为止。在遍历过程中,Iterator 对象负责维护内部状态,以便正确地返回下一个元素。

在迭代器模式中,迭代器对象将遍历算法和集合对象分离开来,避免了暴露集合内部实现细节,并将集合的遍历行为抽象为一个独立的接口。这使得代码更灵活、可扩展、易于维护。

比如,在 Java 中,可以使用 ArrayListLinkedListHashSet 等不同类型的集合类,并使用 Iterator 接口来实现对它们的遍历,而无需关心它们内部的实现细节。

3、迭代器模式总结

迭代器模式是一种行为型设计模式,它提供了一种访问集合对象内部元素的方式,而不用暴露集合的内部细节。

在迭代器模式中,集合对象和迭代器对象分别负责实现集合和遍历算法,并相互独立地进行演化和修改。这样可以避免暴露集合内部结构,也方便对集合进行扩展和修改。

迭代器模式主要由四个角色组成:

  • 抽象聚合类(Aggregate):定义集合对象的接口,包括添加、删除元素等方法。
  • 具体聚合类(ConcreteAggregate):实现抽象聚合类接口,存储集合中的元素。
  • 抽象迭代器类(Iterator):定义遍历集合的接口,包括获取下一个元素、判断是否还有下一个元素等方法。
  • 具体迭代器类(ConcreteIterator):实现抽象迭代器接口,负责对集合进行遍历操作。

迭代器模式的优点包括:

  • 将集合对象和遍历算法分离,使得代码更灵活、可扩展、易于维护。
  • 对客户端隐藏集合对象的内部实现,提高了代码的安全性。
  • 支持对同一种数据结构进行不同方式的遍历。

迭代器模式的缺点包括:

  • 需要实现迭代器对象和聚合对象,增加了代码复杂度。
  • 在集合内部元素发生变化时,需要及时更新迭代器状态,否则可能导致遍历结果不正确。

现在很多的开发语言都已经内置了迭代器模式,不要我们自己再去定义组件的迭代器(不能排除有特殊需求,还是得自己定义迭代器)

比如:

  1. 在 C# 中,集合类内部实现了 IEnumerableIEnumerator 接口,这两个接口就对应了迭代器模式中的抽象聚合类和抽象迭代器类。同时,C# 也提供了 yield 关键字,方便使用迭代器模式进行集合遍历。
  2. Python 中的 __iter__()__next__() 方法,Ruby 中的 each 方法等
  3. 在 Java 中,集合框架中的 Iterator 接口和相应的实现类就是迭代器模式的典型实现。同时,Java 8 还引入了基于 Lambda 表达式的 Stream API,它提供了非常便捷的集合遍历方式,并且可以进行各种数据处理和转换操作,进一步提高了代码的可读性、可维护性和可重用性。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

设计模式-迭代器模式 的相关文章

  • 如何使用 Apache POI API 将图像添加到 pptx 中添加的图像占位符?

    我已经预定义了带有文本和图像占位符的 pptx 模板 我如何从模板访问和修改这些占位符 我可以使用 POI pptx API 直接将图像和文本添加到幻灯片中 但如何将其添加到模板的占位符中 请参阅链接以了解如何添加占位符来创建固定模板 ht
  • Java:迭代 Collection 的最佳方法(此处为 ArrayList)

    今天 当我看到一段我已经使用了数百次的代码时 我很高兴地开始编码 迭代集合 此处为 ArrayList 出于某种原因 我实际上查看了 Eclipse 的自动完成选项 这让我想知道 在什么情况下以下循环比其他循环更好使用 经典的数组索引循环
  • org.apache.sling.api.resource,version=[2.3,3) -- 无法解析

    您好 我无法访问我的项目内容 我已经上传了从 CQ 访问内容所需的所有包 我唯一能看到的是 org apache sling api resource version 2 3 3 无法解析 这是否是异常的原因 如果是 请告诉我如何解决 中Q
  • OpenCV 中的 Gabor 内核参数

    我必须在我的应用程序中使用 Gabor 过滤器 但我不知道这个 OpenCV 方法参数值 我想对虹膜进行编码 启动 Gabor 过滤器并获取特征 我想对 12 组 Gabor 参数值执行此操作 然后我想计算 Hamming Dystans
  • 比较两个文本文件的最快方法是什么,不将移动的行视为不同

    我有两个文件非常大 每个文件有 50000 行 我需要比较这两个文件并识别更改 然而 问题是如果一条线出现在不同的位置 它不应该显示为不同的 例如 考虑这个文件A txt xxxxx yyyyy zzzzz 文件B txt zzzzz xx
  • 运行具有外部依赖项的 Scala 脚本

    我在 Users joe scala lib 下有以下 jar commons codec 1 4 jar httpclient 4 1 1 jar httpcore 4 1 jar commons logging 1 1 1 jar ht
  • 使用 AES SecretKey 的 Java KeyStore setEntry()

    我目前正在 Java 中开发一个密钥处理类 特别是使用 KeyStore 我正在尝试使用 AES 实例生成 SecretKey 然后使用 setEntry 方法将其放入 KeyStore 中 我已经包含了代码的相关部分 The KS Obj
  • Java 文件上传速度非常慢

    我构建了一个小型服务 它从 Android 设备接收图像并将其保存到 Amazon S3 存储桶中 代码非常简单 但是速度非常慢 事情是这样的 public synchronized static Response postCommentP
  • 如何使用 Maven 打包并运行具有依赖项的简单命令行应用程序?

    我对 java 和 Maven 都是全新的 所以这可能非常简单 如果我遵循maven2hello world此处的说明 http maven apache org guides getting started maven in Five m
  • 具有 java XSLT 扩展的数组

    我正在尝试使用 java 在 XSLT 扩展中使用数组 我收到以下错误 Caused by java lang ClassCastException org apache xpath objects XObject cannot be ca
  • Java 中的“Lambdifying”scala 函数

    使用Java和Apache Spark 已用Scala重写 面对旧的API方法 org apache spark rdd JdbcRDD构造函数 其参数为 AbstractFunction1 abstract class AbstractF
  • Javafx过滤表视图

    我正在尝试使用文本字段来过滤表视图 我想要一个文本字段 txtSearch 来搜索 nhs 号码 名字 姓氏 和 分类类别 我尝试过在线实施各种解决方案 但没有运气 我对这一切仍然很陌生 所以如果问得不好 我深表歉意 任何帮助将不胜感激 我
  • 欧洲中部时间 14 日 3 月 30 日星期五 00:00:00 至 日/月/年

    我尝试解析格式日期Fri Mar 30 00 00 00 CET 14至 日 月 年 这是我的代码 SimpleDateFormat formatter new SimpleDateFormat dd MM yyyy System out
  • Jersey 客户端请求中未设置 Content-Length-Header

    我正在使用 Jersey Client 访问网络服务 如下所示 response r accept MediaType TEXT PLAIN TYPE header content length 0 post String class 其中
  • 如何在JSTL中调​​用java方法? [复制]

    这个问题在这里已经有答案了 这可能是重复的问题 我只想调用不是 getter 或 setter 方法的方法例如 xyz 类的 makeCall someObj stringvalue Java类 Class XYZ public Strin
  • java.lang.NumberFormatException: Invalid int: "3546504756",这个错误是什么意思?

    我正在创建一个 Android 应用程序 并且正在从文本文件中读取一些坐标 我在用着Integer parseInt xCoordinateStringFromFile 将 X 坐标转换为整数 Y 坐标的转换方法相同 当我运行该应用程序时
  • 游戏内的java.awt.Robot?

    我正在尝试使用下面的代码来模拟击键 当我打开记事本时 它工作正常 但当我打开我想使用它的游戏时 它没有执行任何操作 所以按键似乎不起作用 我尝试模拟鼠标移动和点击 这些动作确实有效 有谁知道如何解决这个问题 我发现这个问题 如何在游戏中使用
  • HQL Hibernate 内连接

    我怎样才能在 Hibernate 中编写这个 SQL 查询 我想使用 Hibernate 来创建查询 而不是创建数据库 SELECT FROM Employee e INNER JOIN Team t ON e Id team t Id t
  • 为什么C++代码执行速度比java慢?

    我最近用 Java 编写了一个计算密集型算法 然后将其翻译为 C 令我惊讶的是 C 的执行速度要慢得多 我现在已经编写了一个更短的 Java 测试程序和一个相应的 C 程序 见下文 我的原始代码具有大量数组访问功能 测试代码也是如此 C 的
  • 为什么这个作业不起作用?

    我有课Results which extends ArrayList

随机推荐

  • @ComponentScan中的basePackages

    这个注解一般都放在启动类上 作用 扫描包或者是类 如果扫描的是包 那么basePackages这个参数就可以省略 如果扫描的是类 那么这个参数就不可以省略 如果要加basePackages 格式如下 ComponentScan basePa
  • 远程连接服务器显示:发生身份验证错误 要求的函数不受支持

    问题不是出在服务器 而是出在远程桌面客户端 在微软打了补丁后 相当于限制了权限 修改要去登录服务器的那台客户端电脑就可以了 客户端操作系统 win7专业版 双击 加密Oracle修正 然后重新打开远程登录客户端就可以登录了
  • 【机器学习】5、模型融合与调优

    文章目录 一 模型选择 1 1 模型的选择 1 2 超参数的选择 二 模型效果优化 2 1 不同模型状态的处理 2 2 线性模型的权重分析 2 3 Bad case分析 2 4 模型融合 一 模型选择 1 1 模型的选择 确定场景 划分为模
  • FIO常用命令-自学(1)

    fio学习分享 1 根据fio Spec 第一部分为Command line options 以下几个参数比较常用 一 debug type 在debug或学习fio其他参数工作可以使用此命令 例debug io可以查看io的队列情况 也能
  • echartGL中option各3D配置属性配置详解

    最近 接手一个项目需要实现echart中各种3D图表样式 我还是一如既往的从研究echart配置项中各属性含义开始入手 由于自己需要实现的是柱状图效果 类似地图3D 效果在这里不展示 直接进入自己所研究的内容 1 grid3D 希望上面的图
  • vue - H5 移动端网页中调用手机摄像头扫描二维码 / 条形码功能,在h5手机网页里调起相机摄像头进行二维码或条形码扫描效果详细教程(详细示例组件源码,一键复制开箱即用!)

    效果图 实现了在vue网页项目中 实现调起手机摄像头进行扫描二维码或者条码 可自定义样式 直接复制组件代码 然后根据你的需求改一下 lt
  • jacobi迭代法_迭代法解线性方程组、Jaboci迭代法、Gauss–Seidel迭代法、松弛法

    适用方程组 系数矩阵为大型稀疏的方阵 引例 迭代法可以用来解方程 考虑解下面简单的方程 既然都说要用迭代法求解 就不能用正常的同除1 5求解 利用迭代法 把方程改写为以下形式 也可以看作为 你 与 的交点 画出交点的话更加清晰 设 带入方程
  • 商业框架AB包的 原理分析

    1 prefab 的单独 把所有依赖项全部打包到一个包里 加载prefab 的时候 其ab依赖的资源就在包里边 2 通用的资源在外打包到一个包里 打包时会遍历所有资源 然后把资源打包成一个个ab包 需要加载什么文件 就去加载 3 xml 中
  • 【程序人生】做了低薪运营6年,靠什么转行拿下 20W 年薪?

    本人大学专业非计算机相关 毕业六年一直从事运营相关工作 在上家公司的新业务系统项目中 因为项目组人手不足兼做了部分功能测试 让我对测试工作产生了浓厚的兴趣 后来 在xxx学习后 我从一个运营妹纸成功转型成为某世界500强公司的外包 年薪近2
  • MS17-010漏洞攻击与防御(利用永恒之蓝攻击Win7系统)

    任务1 利用永恒之蓝攻击Win7系统 在Kali终端中输入命令 msfconsole 启动Metasploit 输入命令 use auxiliary scanner smb smb ms17 010 加载扫描模块 输入命令 set RHOS
  • Boost电路硬件设计实例

    上一篇 Boost电路原理分析及其元件参数设计 Vane Zhang的博客 CSDN博客 本文以单相光伏发电系统中前级Boost电路为例对其进行硬件设计 Boost电路的硬件电路主要包括能量转换电路 开关管驱动电路和信号检测电路的设计 1
  • 半导体八大工艺流程图_大国重器,国芯基石 半导体离子注入机行业研究报告...

    主要观点 掺杂是指在硅晶体中加入少量的杂质元素 以此改变衬底材料的电学性质 是半导体加工制造过程中关键的工艺技术 根据掺杂的技术原理 该工艺可分为热扩散和离子注入 由于离子注入技术可以在芯片制成尺寸更小 空间结构更复杂的情况下实现元素掺杂
  • MCL2 -1.1.1

    大家好 承诺大家已久的1 1游戏体验优化 历经一个星期 也终于是完成了1 1 1版本 这个版本更新内容其实蛮多的 接下来就请大家拭目以待 更新目录 更新内容 更新代码 应用程序 更新内容 游戏封面体验增强 修复末影龙打完会闪退的bug 文件
  • C#企业微信 接收事件服务器(添加外部联系人事件)#openapi回调地址请求不通过# 完整源代码

    设置接收事件服务器 openapi回调地址请求不通过 企业微信api 添加外部联系人事件 using System Web UI WebControls using System IO using System Text using Sys
  • 从文本文件读取文件名,并删除指定路径下的文件

    void deleteFiles const QString path const QString delFileNameTxt const QString recycleBinPath QStringList delFileNames Q
  • 很多软件安装时为什么需要设置环境变量

    设置环境变量的目的 背景 在cmd中想要执行net start mysql等操作命令 必须先cd到bin文件所在目录 如D mysql mysql x x xx winx64 bin 那么每次打开mysql 都要输入那么多指令切换目录是不是
  • ME2M/ME3M增强 - ALV显示里增加字段(原)

    i s 本帖最后由 lulu1212 于 2012 10 12 11 23 编辑 之前用户提出要在ME2M ME3M里加字段 网上找了些资料发现是在结构里APPEND字段就可以了 结构名是 MEREP OUTTAB PURCHDOC 例如
  • RH850学习笔记

    这是一篇关于瑞萨RH850系列单片机的心得 网上关于这方面的资料特别少 可能是使用的人较少的原因吧 由于工作的原因 这段时间接触了这款单片机 所以写下了这篇博客 希望能给大家一些帮助 R7F701023 1023F1L是我最早接触的瑞萨MC
  • 算法笔记——动态规划

    算法笔记 动态规划 动态规划是一个非常灵活的算法 动态规划本身不难 无非就是一个状态转移的过程 难点就在于我们该如何去定义 状态 而这就需要我们多做题来积累经验 这也是初学者遇到动态规划往往无从下手的原因 动态规划的核心在于状态和状态转移方
  • 设计模式-迭代器模式

    迭代器模式 1 迭代器模式介绍 迭代器模式是一种行为型设计模式 它提供了一种方法来访问聚合对象中的各个元素 而不暴露其内部表示 通过使用迭代器 客户端可以遍历一个聚合对象中的元素 而不必了解其内部实现 在迭代器模式中 定义了一个迭代器接口