Java接口实战:从语法机制到设计原则与常见坑

发布时间:2026/10/1 14:57:31
Java接口实战:从语法机制到设计原则与常见坑 接口在Java里的地位我觉得怎么强调都不过分。很多初学者写着写着就发现自己写的代码一旦要加需求就像往行李箱塞衣服——硬塞能塞进去但拉链快爆了。而接口本质上就是你提前说好行李箱能装多少、怎么分层让后面的代码不用拆箱子也能扩展。这篇新Java基础二十四接口我会用实际项目里的思路来讲清楚接口到底解决什么问题、语法上有哪些容易被忽略的细节、跟抽象类怎么选、以及日常开发里那些让人挠头的坑。适合刚开始学面向对象不久、或者写了一阵子代码但对接口只停留在知道概念、用不深刻的朋友。1. 接口到底解决什么问题先说设计层面的价值1.1 从协议的角度看待接口而不是语法很多教程一上来就讲interface怎么定义、怎么implements然后举一个猫啊狗啊的动物例子学生听完一脸懵这玩意儿不就是个空壳吗我直接写类继承不也行问题的根源在于我们太早进入语法细节反而忽略了接口存在的真实动机——它是一份协议是调用方与实现方之间的契约。举一个贴近生活的例子。你去餐厅点餐服务员递给你一份菜单菜单上写着菜名和价格。你不需要知道后厨是怎么炒菜的也不需要关心厨师用的是什么锅你只关心按菜单点厨房能给我端出对应的菜。菜单就是接口厨房就是实现类。只要菜单不变哪怕后厨把厨师换了、把煤气灶换成电磁炉食客这边完全不受影响。反过来只要后厨严格按照菜单做菜哪怕是换了新厨师也不会出现客人点了宫保鸡丁、端上来鱼香肉丝的情况。这就是接口的第一大作用把做什么和怎么做分离。调用方食客只依赖菜单接口不依赖具体的实现后厨细节。在代码里这意味着你的业务层不需要知道数据到底从数据库来、从缓存来、还是从远程HTTP接口来它只需要面向一个接口编程具体实现随便换。这个思路落地到代码中就是面向接口编程。比如一个订单服务内部要调用支付能力你不会直接在订单服务里new一个AlipayService而是定义一个PaymentService接口在里面声明pay(Order order)方法。订单服务只持有这个接口真正创建支付实现的活儿交给Spring容器或者其他组装代码去做。这样一来今天接支付宝明天接微信支付后天甚至接一个跨境支付渠道订单服务一行都不用改。1.2 依赖倒置与接口隔离接口背后的两条设计原则既然说到设计层面就绕不开SOLID原则里的两个关键项依赖倒置原则DIP高层模块不应该依赖低层模块两者都应该依赖抽象抽象不应该依赖细节细节应该依赖抽象。翻译成人话就是业务代码不应该直接依赖工具代码业务代码和工具代码都应该依赖接口。比如发送短信这件事一个注册服务如果直接依赖阿里云短信SDK那下次换成腾讯云短信、换成自研短信通道注册服务就得跟着改。如果注册服务依赖的是一个SmsSender接口底下有一堆实现类那注册服务永远不用动。接口隔离原则ISP客户端不应该被迫依赖它不使用的接口。这句话的意思是如果一个大接口里有10个方法而某个实现类只用到其中2个那这个接口就该拆了。否则实现类就得为了用2个方法白白实现另外8个空方法或者抛异常非常别扭。接口隔离原则在真实项目里的典型场景是配置刷新。假设你有一个DynamicConfigService接口里面有getString(key)、getInt(key)、refresh()、addListener(listener)四个方法。消耗配置的业务方只需要前两个读方法但管理层需要后两个刷新和监听方法。如果只用一个接口业务方就会看到刷新和监听的方法容易误用也增加了mock测试时的负担。更合理的做法是拆成ConfigReader接口提供读取方法和ConfigManager接口在ConfigReader基础上提供刷新、监听方法这样业务方依赖最小化。接口在这里不只是语法它强制你把协议想清楚。你定义接口的时候其实是在回答一个问题我这模块对外承诺了什么能力承诺得越清晰、越小模块的屁股就越干净将来越好替换、好测试。2. 接口语法演进从最基础到容易被忽略的细节2.1 定义一个接口并实现它最基础的姿势先看看最经典的写法。定义一个接口声明一个抽象方法然后让类去实现public interface PaymentService { /** * 执行支付 * param order 订单信息 * return 支付结果 */ boolean pay(Order order); } public class AlipayServiceImpl implements PaymentService { Override public boolean pay(Order order) { // 调用支付宝SDK的具体逻辑 System.out.println(使用支付宝支付订单号 order.getOrderNo()); return true; } }几个关键细节值得注意接口里的方法默认是public abstract的即使你不写这两个关键字编译后也会自动加上。所以接口方法只能是公开的不可能是什么protected或者privateJava 9以后有私有方法那是另一回事后面细说。实现类中的方法必须用public修饰。这一点特别容易被新手踩坑因为实现方法时你想省事直接写成boolean pay(Order order)编译器立刻报错——说什么attempting to assign weaker access privileges。原因很简单接口方法本来就是public你实现的时候权限不能比public小否则外部通过接口调用就访问不到这个方法了。实现类可以用Override注解虽然不写也能编译但写上能让你及时发现签名写错的问题。比如接口方法参数是Order你实现时写成了order少个类型或者方法名拼错了Override会直接编译报错相当于多了一道保险。我见过很多刚入行的同学接口方法签名改来改去结果实现类忘了同步更新运行时报AbstractMethodError——这就是典型的签名不一致问题。解决方式就是改接口方法时全项目编译一遍别只编译自己那个模块。2.2 默认方法与静态方法Java 8给接口带来的转折Java 8之前接口里只能有抽象方法这就导致一个问题当JDK想给一个已有接口加新方法时所有实现类都必须跟着改。比如List接口如果加一个forEach方法那所有实现List的类都得手写一遍实现这显然不现实。于是Java 8引入了默认方法default method。用default关键字修饰方法带上方法体实现类可以选择重写也可以直接用默认实现public interface PaymentService { boolean pay(Order order); // 默认方法支付成功后发送通知默认不发送实现类按需重写 default void sendNotification(Order order) { System.out.println(支付成功默认不发送通知); } }这个特性在实际开发中最大的价值是平滑演进。比如你有一个接口被项目里20个类实现了现在所有支付场景都要加一个支付前校验幂等的扩展点。如果往接口里加抽象方法20个类都得改但如果提供默认方法一个都不用动需要扩展的类重写即可。同时Java 8还允许接口里定义静态方法。接口里的静态方法跟类的静态方法类似属于接口本身不属于实现类public interface PaymentService { static PaymentService createDefault() { return new AlipayServiceImpl(); } }调用方式就是PaymentService.createDefault()。静态方法通常用来提供默认工厂方法或者工具逻辑。比如你有个FileStorage接口里面可以写一个静态方法createLocalStorage(String basePath)返回一个本地文件存储实现方便没有Spring注入的场景快速使用。关于默认方法有一个坑必须说默认方法与接口继承、类继承发生冲突时规则比较绕。如果一个类实现两个接口两个接口里有同名同参的默认方法那么这个类必须重写该方法否则编译报错。更复杂的情况是类优先原则如果一个类的父类里有一个具体方法跟接口默认方法同名同参那么类的实现优先于接口默认方法。这条规则我在后面常见问题章节还会展开讲。2.3 Java 9之后的私有方法让默认方法之间不重复Java 9给接口加了一个新能力——私有方法。接口里可以写private方法这些方法只能在接口内部被默认方法或者静态方法调用实现类看不见外部也看不见。你可能要问这有什么用答案是消除默认方法之间的重复代码。比如你有三个默认方法都要做参数校验区别只是校验完之后调用的逻辑不同。如果每段都写一遍参数校验代码就重复了。这时候可以把公共逻辑抽成一个private方法public interface PaymentService { boolean pay(Order order); default void payByBalance(Order order) { checkOrder(order); System.out.println(余额支付); } default void payByCoupon(Order order) { checkOrder(order); System.out.println(优惠券支付); } private void checkOrder(Order order) { if (order null || order.getAmount() 0) { throw new IllegalArgumentException(订单不合法); } } }注意这里的private方法是Java 9才有的如果你的项目基于Java 8别用编译直接不通过。我见过有些同学在Java 8项目里写接口私有方法编译报错后一脸茫然——其实就是版本没搞明白。2.4 接口字段全部都是常量别指望它存状态很多新手会疑惑接口里能不能定义变量答案是接口里的字段只能是public static final的常量即使你不写这三个修饰符编译器也会自动给你补上。换句话说接口没有实例字段它不能存储对象状态。这背后的设计逻辑很清晰接口描述的是能做什么而不是有什么。状态属于实现类的内部事务跟协议无关。如果你真的在接口里定义了一个变量比如public interface PaymentService { int CODE_SUCCESS 200; int CODE_FAIL 500; }这里的CODE_SUCCESS和CODE_FAIL就是常量使用方式是PaymentService.CODE_SUCCESS而且必须在定义时初始化。有些团队喜欢把这类常量放在接口里当常量池用但从代码整洁角度我不太推荐——常量应该放在真正归属的业务类里接口存在的意义还是方法的契约。Java 17里接口还引入了sealed相关的能力密封接口允许限制哪些类可以实现该接口。这块属于偏进阶的语法用在框架设计场景比较多基础阶段知道名字即可。3. 接口与抽象类到底怎么选别靠背3.1 设计理念差异契约 vs 模板骨架我在面试中经常问候选人一个问题接口和抽象类有什么区别很多人能背出接口多继承、抽象类单继承接口方法默认public abstract、抽象类可以有具体方法接口字段是常量、抽象类可以有实例字段这些条条框框但问到你的代码什么场景用接口、什么场景用抽象类就支支吾吾了。我个人的理解是这样的接口是能做什么的协议抽象类是是什么的模板骨架。接口通常用-able或者-or这类后缀命名比如Runnable可运行的、Comparable可比较的、PaymentService支付服务强调的是能力。抽象类通常是对一类事物进行模板化比如AbstractHttpServlet一个HTTP处理模板、BaseExportHandler一个导出流程骨架强调的是这是什么东西它的大体流程长什么样。这里有一个操作性很强的判断依据当你关心的是调用方怎么写代码时用接口当你关心的是实现方怎么复用代码时用抽象类。前者解决外部怎么用后者解决内部怎么省事。3.2 实际项目中的选择场景场景一策略模式。你有多种支付方式每种支付方式的入参、出参一致但算法不同。这时候必然选接口。因为调用方只需要面向PaymentService不需要关心底下是支付宝还是微信。如果选了抽象类虽然也能实现多态但等于强迫所有实现类继承同一个父类这不好——万一某个实现类已经有父类了呢Java是单继承抽象类直接把扩展路径堵死了。场景二模板方法模式。你要导出一个报表导出流程是固定的查询数据 - 格式化数据 - 生成文件 - 上传存储。每个步骤在不同报表里细节不同但骨架一致。这时候适合抽象类把所有流程定义好把变化的步骤做成抽象方法让子类填充。用接口也可以做但得把公共骨架逻辑抽到一个工具类里代码不如抽象类简洁。场景三既要模板骨架又要对外契约。这是很多框架实际做的事一个抽象类实现了某个接口把公共逻辑写好只留扩展点给子类。比如AbstractPaymentService implements PaymentService它把参数校验 幂等检查 加锁这些公共流程都写死再把doPay留给子类。这样外部依赖PaymentService内部实现复用AbstractPaymentService两头的好处都占了。这个组合模式是Java开发里最常见的套路值得记下来。我再补充一个实操判断点多态的场景先想接口代码复用的场景先想抽象类。如果一个类既是某种东西又有某个能力往往优先用类继承解决是什么再用接口实现解决能做什么。举个库里的例子ArrayList是抽象类AbstractList的子类同时又实现了List、RandomAccess、Cloneable、Serializable等多个接口。这就是一个类一个本质 多种能力的典型模型。4. 项目实战用接口重构一段代码4.1 先看一段没有接口的坏味道代码假设你现在接手一个电商项目里面有一个下单后通知用户的工具类。最初的代码可能是这样的public class OrderNotifier { private EmailSender emailSender; public OrderNotifier() { this.emailSender new EmailSender(); } public void notifyUser(Order order) { String message 您的订单 order.getOrderNo() 已生成请及时支付; emailSender.send(order.getUserEmail(), 订单通知, message); } }这个代码有什么问题问题大了。通知方式被写死成邮件如果产品经理明天说要加短信通知、加站内信通知、加微信模板消息通知这个类就得不停地改if分支。更致命的是OrderNotifier自己new EmailSender()导致单元测试时很难模拟邮件发送失败的情况——你没法用测试替身。4.2 通过接口拆解定义协议、实现替换第一步定义一个通知接口public interface UserNotifier { void send(Order order, String message); }第二步把邮件发送做成接口实现public class EmailNotifier implements UserNotifier { Override public void send(Order order, String message) { // 具体发送邮件的逻辑依赖注入 EmailSender System.out.println(发送邮件给 order.getUserEmail() message); } }第三步通知服务只面向接口public class OrderNotifier { private UserNotifier notifier; // 通过构造方法注入而不是自己new public OrderNotifier(UserNotifier notifier) { this.notifier notifier; } public void notifyUser(Order order) { String message 您的订单 order.getOrderNo() 已生成请及时支付; notifier.send(order, message); } }重构完之后通知方式变成了可插拔的。在单元测试里你可以轻轻松松写一个MockNotifier实现UserNotifier用来断言消息是否被发送出去public class MockNotifier implements UserNotifier { private ListString messages new ArrayList(); private Order lastOrder; Override public void send(Order order, String message) { this.lastOrder order; this.messages.add(message); } public ListString getMessages() { return messages; } }然后在测试里替换掉真实发送器断言通知内容是否符合预期。这个改动背后真正起作用的是接口 依赖注入的组合。前者定义了协议后者让协议的使用方不负责创建具体实现。4.3 配合JDK动态代理实现更灵活的扩展接口还有一个非常有用的特性配合JDK动态代理。java.lang.reflect.Proxy只能代理接口不能代理没有接口的类。这意味着如果你的代码面向接口设计那么做AOP切面编程时就特别方便——比如在不修改EmailNotifier源码的情况下给所有通知方法加上日志、加上重试、加上监控。一个最简单的动态代理例子给UserNotifier添加方法耗时统计UserNotifier target new EmailNotifier(); UserNotifier proxy (UserNotifier) Proxy.newProxyInstance( UserNotifier.class.getClassLoader(), new Class[]{UserNotifier.class}, (proxyInstance, method, args) - { long start System.currentTimeMillis(); Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println(方法 method.getName() 耗时 cost ms); return result; } );Spring的Transactional、Async等注解底层大量使用这种机制。你面向接口编程Spring才能在你调用notifier.send()时悄无声息地织入事务、日志、异常处理等逻辑。如果你直接依赖EmailNotifier这个类Spring想拦截就麻烦了得用CGLIB生成子类代理限制更多一些。所以我在实际项目里有个习惯跨模块的调用一律通过接口暴露。模块内部的类想怎么new就怎么new但模块之间的依赖我必须用接口隔开。这样后续做单元测试、做代理增强、做实现替换都留好了后路。5. 接口在真实框架与面试中的角色5.1 JDK里那些你天天用但不觉得是接口的接口Java开发天天跟接口打交道只是很多初学者没意识到。比如List、Set、Map全是接口你写ListString list new ArrayList()本质上就是面向接口编程。你换一个LinkedList实现代码其他地方完全不用改这就是接口替换能力在集合框架里的体现。Comparable接口你的类实现compareTo方法就能用Collections.sort()排序。这也是一个典型的能力契约——只要你的对象可以被比较排序工具就能处理它。Runnable接口创建线程时可以传一个Runnable对象线程执行的逻辑跟你具体的线程管理策略解耦。Iterator接口集合遍历的协议。你在for-each里写的操作底层全是Iterator在干活你甚至可以让自己的类实现Iterable然后用for-each遍历这是一种很好的领域模型设计技巧。面试的时候我经常建议候选人从这些日常接口入手讲清楚我在用什么接口、它解决了什么问题远比干巴巴背诵interface语法有说服力。5.2 框架里接口的三种典型用法第一个用法策略接口的注入。Spring里你写一个PaymentStrategy接口用Component实现多个策略MapString, PaymentStrategy自动注入所有策略实现类然后通过paymentStrategyMap.get(strategyId)来选择策略。这个模式在项目里处理多种类型、同一种操作时特别好用比如不同订单类型走不同计价规则、不同渠道走不同清结算逻辑。第二个用法SPI机制。java.util.ServiceLoader通过接口而非实现类去加载扩展实现很多中间件、日志框架如SLF4J绑定logback或者log4j2就是这么做的。你写代码时面向org.slf4j.Logger这个接口实际的日志实现通过SPI机制在运行时确定这意味着你的日志代码跟具体日志框架解耦了。这是接口在可插拔架构里的完美示范。第三个用法模板接口。Spring里的InitializingBean有一个afterPropertiesSet()方法、ApplicationListener监听应用事件只要实现这些接口Spring容器在特定时间点就会回调你的方法。这种回调接口机制巧妙地把控制权从框架反转给业务代码底层借助的是接口多态 容器扫描。框架用接口的设计思路总结起来就一句话框架提供上下文和控制流业务通过实现指定接口来参与其中。你理解了这个模式以后看任何框架源码都会有章法。5.3 高频面试考点从概念到实战接口在Java面试里出现频率极高我把常见考点和推荐答案思路列一下问接口和抽象类有什么区别 答语法层面有三处主要区别接口支持多实现抽象类只能单继承接口字段必须是public static final常量抽象类可以有实例字段接口方法默认public abstractJava 8后有默认方法和静态方法抽象类可以有构造器和具体方法。设计层面接口强调能力契约抽象类强调模板骨架项目里经常组合使用。问Java 8为什么引入默认方法 答为了让已经发布的接口在添加新方法时不破坏所有现有实现类。这个问题的核心是二进制兼容性可以举List接口加spliterator等例子。如果回答得好说明你有真实版本演进经验。问一个类可以实现多个接口如果两个接口有同名默认方法怎么办 答实现类必须重写这个冲突的方法否则编译报错。重写时可以调用某个接口的默认实现InterfaceA.super.method()。这个问题考察是否踩过多继承冲突的坑有实际编码经验的人一般能答上。问接口可以实例化吗 答直接new不行接口没有构造器。但可以通过匿名内部类或者Lambda表达式创建接口的实例本质上是创建了一个实现类的匿名对象。Java 8后对于只有一个抽象方法的接口函数式接口Lambda的写法更加简洁。问FunctionalInterface是什么 答标注在只有一个抽象方法的接口上编译器会强制校验你确实只有一个抽象方法。默认方法和静态方法不计入。它保证了接口可以用Lambda表达式表示是Java函数式编程的基础。6. 实操中会踩到的坑与排查技巧6.1 默认方法的多继承冲突规则记不住就试默认方法冲突确实容易让人晕我工作中真遇到过因为这个问题导致的线上Bug。事情是这样的项目里有一个AuditLogService接口后来加了一个默认方法recordLog(String action)另一个历史接口EventBusListener里也有一个默认方法recordLog(String eventName)一个监听器类同时实现了这两个接口编译直接报错。解决方式很简单在实现类里重写这个方法并且显式指定调哪个接口的默认实现。语法是AuditLogService.super.recordLog(action)Java允许这样做public class OrderEventListener implements AuditLogService, EventBusListener { Override public void recordLog(String action) { // 选择其中一个实现或者彻底自己实现 AuditLogService.super.recordLog(action); } Override public void pay(Order order) { // 中略... } }还有一个优先级规则需要记住类的方法优先于接口默认方法。如果一个类继承了父类的public void recordLog(String s)方法同时这个类又实现了带同名默认方法的接口那么最终执行的父类的方法接口默认方法被忽略。这是比较反直觉的一条规则但Java规范就是这么定的。我的建议是能不用默认方法尽量不用尤其是大型项目里。默认方法虽然方便了接口演进但会给选择困难症患者增加心智负担。真要加扩展点优先考虑定义一个新的接口让旧实现类按需实现新接口。6.2 Lambda表达式与函数式接口Java 8之后接口经常跟Lambda绑定。比如Runnable、Comparator、Consumer这些接口你不需要写实现类直接用Lambda表达式ComparatorOrder byAmount (o1, o2) - Double.compare(o1.getAmount(), o2.getAmount());这里有个限制只有函数式接口只有一个抽象方法的接口才能使用Lambda。如果你在接口里不小心写了两个抽象方法Lambda就不能用了。所以如果你的接口设计成给调用方传Lambda用的记得加上FunctionalInterface注解做检查防止后续有人加抽象方法导致编译失败。这个设计在业务里也很常见比如事件回调public interface PaymentCallback { void onSuccess(Order order); } // 调用方直接传Lambda paymentService.pay(order, order - { System.out.println(支付成功更新订单状态 order.getOrderNo()); });这种微接口模式用得好代码会非常简洁清爽。它跟大接口形成鲜明对比——大接口往往是坏味道十几个方法堆在接口里实现类苦不堪言。6.3 接口与序列化、克隆的交互如果你设计了一个接口实现类需要序列化有几个细节要注意。首先序列化接口Serializable是一个标记接口没有任何方法。你的实现类如果通过RPC传输、或者存Redis通常要加上implements Serializable并且定义serialVersionUID。这个ID是序列化版本号如果接口签名改了而ID没改反序列化时字段对不上就会出问题。另外如果你在接口里定义了默认方法它对序列化没有特殊影响但如果你用JDK动态代理生成的对象要序列化得注意代理对象本身能否序列化取决于代理类和被代理对象是否都实现了Serializable。这个细节比较边缘但确实有同事在把代理对象存Redis时踩过坑报NotSerializableException。6.4 接口命名与团队规范这是经验也是坑最后分享一个我踩过很多坑之后的体会。接口命名这件事团队一致性比个人偏好重要得多。有的团队习惯接口前缀大写的I比如IPaymentService有的团队习惯直接用名词或者带-able、-or后缀比如PaymentService、PaymentProvider。我个人更倾向于后者原因有两点第一不用I前缀的话实现类命名空间更自由比如PaymentService接口 AlipayPaymentService实现语义自然第二很多现代框架比如Spring源码里接口命名普遍不带I前缀跟社区主流保持一致新同事进来容易适应。除了命名还有一个重要规范接口应该定义在被依赖方所在的包。比如支付能力如果被订单模块依赖接口PaymentService就应该放在payment-api模块里订单模块只依赖这个api模块不依赖支付实现模块。这是模块化设计的基础也是Maven多模块项目里最常见的依赖关系控制手段。你如果一开始不重视接口的位置项目后期做模块拆分时会改到怀疑人生。6.5 常见问题速查表问题现象原因解决办法编译报错实现方法权限不够实现方法没有用public修饰实现类方法必须显式写public编译报错默认方法冲突类实现了多个带同名默认方法的接口在实现类里重写冲突方法用XxxInterface.super.method()指定运行时AbstractMethodError新增了实现类没实现的方法或者接口方法签名被改过编译全模块确保所有实现类同步更新Lambda表达式报错not a functional interface接口里有不止一个抽象方法加上FunctionalInterface检查或拆分接口序列化报NotSerializableException通过接口引用的对象未实现Serializable确保实现类和代理对象实现Serializable并定义serialVersionUID接口反射拿不到字段值忘了接口字段是public static final常量试图通过实现类构造函数初始化把状态放到实现类中接口只定义常量7. 写在最后我的一点体会接口这个东西越用越能体会到约束即自由的道理。初学的时候总觉得它多余——明明可以直接调用类的方法为什么要绕一圈但当你接手过那种到处new、改一个需求牵连十来个类的老旧系统就会明白提前用接口把边界划清楚等于给你的代码留了呼吸空间。我在实际开发中的习惯是新建一个跨模块的协作点时先停下敲键盘的手花三分钟想一想这个协作点有哪些变化的方向。如果变化方向不止一个那就定义一个接口如果只有一个也要考虑将来测试怎么办。这个思考习惯坚持下来你写的代码会慢慢从能跑变成好改。接口也不是越多越好。一个总共没几个类的小模块里塞十来个接口反而是过度设计。我见过一些刚学设计模式的同学写个HelloWorld都要套三层接口三层抽象类结果代码看完一圈愣是没找到方法在哪。接口设计的度就是多一个太冗余、少一个太僵化的那个位置。怎么拿捏没有捷径就是多看多写多复盘踩了坑就记住了。另外还想提一个很实际的操作写接口时一定要写Javadoc注释把每个方法的用途、参数、返回值、异常都讲清楚。因为接口是给别人看的契约注释写在接口上比写在实现类里重要得多。很多团队里调用方依赖出问题翻开源码注释一看里面只有个空荡荡的方法签名那体验真的劝退。这个系列还有不少内容可以继续往下挖比如泛型与接口的配合、接口在集合框架里的深度应用、用接口做SPI扩展。如果你们感兴趣下一篇我可以把接口和泛型结合的那点事讲透。