1. 为什么这两个关键字总让人犯迷糊刚入行那会儿我在一个后台管理系统里写代码需要定义一个订单服务类。当时脑子里想的是“订单服务是一种服务那就用 extends 继承一个基础服务类吧”结果写着写着发现基础服务类里有一堆跟订单无关的方法全被继承下来了改一个地方牵一发动全身。后来带我的老哥看了一眼说“你这里应该用 implements把能力抽成接口。”那是我第一次真正意识到extends和implements虽然都是 Java 里跟在类名后面的关键字但它们解决的是完全不同维度的问题。这个标题看起来像是面试八股文里的常客但实际工作中踩过坑的人都知道它远不止“一个是继承类一个是实现接口”这么一句能打发。它背后牵扯的是面向对象设计里最核心的两个概念继承Inheritance和抽象Abstraction。你选哪个关键字本质上是在回答“这个新类型和已有类型之间到底是什么关系”——是“是一种is-a”的血缘关系还是“能做什么can-do”的能力契约关系。这篇文章我打算把这两个关键字从语法、语义、设计意图、实际项目选型、常见坑几个层面彻底拆开讲。适合刚学 Java 不久、对这两个关键字还停留在背诵阶段的同学也适合写了几年代码但一直凭感觉用、没系统梳理过的开发者。看完之后你至少能做到拿到一个需求能条件反射地判断这里该用 extends 还是 implements以及为什么。先给一个最粗的结论垫底extends用于类的继承和接口的继承表达的是“复用 扩展”implements用于类实现接口表达的是“承诺 能力”。一个类只能 extends 一个父类但可以 implements 多个接口。这个差异不是随便定的而是 Java 为了规避多继承的菱形问题做出的设计取舍后面会详细展开。2. 从语法层面把两个关键字彻底拆开2.1 extends 的语法形态与适用对象extends在 Java 里出现在两个位置。第一个位置是类的声明处public class Dog extends Animal { // Dog 继承 Animal }第二个位置是接口的声明处public interface SerializableCollection extends Collection, Serializable { // 接口可以多继承接口 }注意这里有个容易被忽略的点类只能单继承但接口可以多继承接口。也就是说extends用在接口上时后面可以跟多个接口名用逗号隔开。这是很多人第一次看到会愣一下的地方——明明说 Java 不支持多继承怎么接口就能 extends 好几个原因在于接口只声明方法签名不携带方法实现Java 8 之前的认知所以不存在多个父接口之间实现冲突的问题菱形问题在纯抽象层面不会造成歧义。而类用extends时后面只能跟一个父类。这个限制是 Java 语言层面的硬约束编译器直接卡死你写两个父类连编译都过不了。2.2 implements 的语法形态与适用对象implements只出现在类的声明处后面可以跟一个或多个接口public class ArrayListE extends AbstractListE implements ListE, RandomAccess, Cloneable, java.io.Serializable { // ... }这是 JDK 里ArrayList的真实声明非常典型。它 extends 了一个抽象父类AbstractList同时 implements 了四个接口。这个声明本身就说明了一件事extends 和 implements 可以在同一个类声明里同时出现而且 extends 必须写在 implements 前面。顺序写反了编译器会报错这是语法硬规定没有为什么记住就行。implements后面跟的接口数量没有上限你想实现多少个都行只要你的类把所有接口里的抽象方法都实现了或者类本身声明为 abstract。这也是 Java 用接口来模拟“多继承能力”的核心手段。2.3 一张表看清语法差异对比维度extendsimplements出现位置类声明、接口声明仅类声明后面可跟数量类场景1 个父类任意多个接口后面可跟数量接口场景任意多个父接口不适用与对方共存时的顺序必须在前必须在后是否强制重写方法否可选择性重写是除非类是 abstract能否继承字段能非 private 字段不能接口字段都是常量能否继承方法实现能Java 8 后能继承 default 方法这张表建议直接记住面试和实际写代码时都用得上。尤其是“能否继承字段”这一条很多人没意识到接口里的字段其实都是public static final的常量你 implements 一个接口并不会把它的字段“继承”成实例字段只是能直接通过简单名访问这个常量而已。3. 语义层面的本质区别is-a 与 can-do3.1 extends 表达的是“是一种”的血缘关系当你写class Dog extends Animal你是在向整个代码库宣告一个本体论级别的事实狗是一种动物。这个关系是分类学意义上的具有传递性——狗是动物动物是生物所以狗也是生物。Java 里 extends 的继承链也是传递的Dog的实例可以赋值给Animal类型的变量也可以赋值给Biology类型的变量如果 Animal extends Biology 的话。这种关系带来的直接后果是父类的所有非私有成员字段和方法都会成为子类的一部分。子类天然拥有父类的全部能力并且可以在此基础上增加新能力或修改已有行为通过方法重写。这就是代码复用的核心机制。但血缘关系也意味着强耦合。父类改一个方法的签名所有子类都可能受影响。父类的构造器逻辑变了子类的初始化顺序也可能出问题。所以继承被很多设计原则比如《Effective Java》里的建议视为一种“强耦合”手段能少用就少用优先考虑组合。3.2 implements 表达的是“能做什么”的能力契约当你写class Bird implements Flyable你表达的不是“鸟是一种飞行器”而是“鸟具备飞行的能力”。这个关系是能力标签意义上的不具有传递性——鸟能飞不代表鸟的某个属性也能飞。接口定义了一组方法签名实现类承诺提供这些方法的具体实现。接口的核心价值在于解耦调用方和实现方。调用方只依赖接口不关心具体是哪个类实现的。比如你写一个feed(Flyable f)方法任何实现了 Flyable 的对象都能传进来不管是鸟、飞机还是超人。这种“面向接口编程”的能力是 Java 实现多态和依赖倒置的基础。接口还有一个隐含语义实现类必须完整履行契约。接口里声明的每个抽象方法实现类都必须给出实现否则编译不过除非实现类是抽象类。这个强制性保证了“只要一个对象声称实现了某接口你就一定能调用接口里的所有方法”这是接口作为契约的信任基础。3.3 用生活类比把两者讲透打个比方。extends像是家族继承——你继承了你父亲的姓氏、房产和一部分性格这些东西是你与生俱来的一部分你没法选择不要除非用特殊手段。你和你父亲是“是一种”的关系你是这个家族血脉的延续。implements像是职业资格证书——你考了注册会计师证说明你具备做账的能力但你本身不是“一种证书”。你可以同时持有多个证书注册会计师、律师证、驾照这些证书之间互不冲突只是给你贴上了不同的能力标签。别人找你办事只看你有没有对应的证书不关心你姓什么、从哪来。这个类比基本能覆盖 90% 的场景判断。当你纠结用哪个时问自己一句这个新类型和已有类型之间是“是一种”还是“能做什么”答案就出来了。4. 设计意图与选型逻辑什么时候必须用哪个4.1 优先用接口的四个典型场景场景一需要定义一组跨类别的能力。比如Comparable、Serializable、Runnable这些接口它们的实现类可能分布在完全不同的继承体系里。一个User类和一个Order类都可以实现Serializable但它们显然没有共同的父类。这种跨类别的能力标注只能用接口。场景二需要多继承能力。Java 类只能单继承如果你已经 extends 了一个父类还想再获得另一组能力只能通过 implements 接口。比如ArrayList已经 extends 了AbstractList还想具备随机访问、克隆、序列化能力就 implements 了四个接口。场景三需要解耦调用方和实现方。这是接口最重要的工程价值。你的业务代码依赖PaymentService接口而不是依赖AlipayPaymentService具体类。将来换成别的支付渠道只需要新增一个实现类业务代码一行不用改。这种可替换性是大型项目可维护性的命脉。场景四需要定义 SPI服务提供者接口或回调。框架给你留一个接口你实现它框架在合适的时机回调你的方法。比如线程池的ThreadFactory、事件监听器的EventListener都是这个套路。框架不可能知道你具体的类是什么只能通过接口来约定交互方式。4.2 谨慎使用 extends 的判断标准继承不是不能用而是要用在刀刃上。我个人的判断标准是只有当子类确实是父类的一种特化并且父类的设计本身就是为继承准备的有文档说明、有 protected 构造器、有清晰的扩展点才考虑用 extends。具体来说满足以下条件时用 extends 比较合适子类和父类在概念上确实是 is-a 关系且这个关系在未来可预见的时间内不会改变。父类提供了子类真正想复用的代码方法实现或字段而不是仅仅为了统一类型。父类的作者明确允许继承比如 JDK 里的AbstractList、AbstractMap就是专门为继承设计的抽象类。你不需要从多个来源复用代码因为单继承限制。反过来如果只是为了复用几个方法而继承或者父类本身是个具体类、没有为继承做任何设计那大概率应该用组合把父类对象作为字段持有而不是继承。这是《Effective Java》里“组合优于继承”的核心论点我在实际项目里踩过好几次坑之后深以为然。4.3 抽象类与接口的配合使用实际项目里最常见的模式是抽象类 extends 抽象类具体类 extends 抽象类并 implements 多个接口。抽象类负责提供部分公共实现和模板方法接口负责定义能力契约。比如 JDK 的集合框架就是教科书级别的示范Collection接口定义集合的基本契约。AbstractCollection抽象类提供部分默认实现。List接口 extendsCollection增加列表特有的契约。AbstractList抽象类 extendsAbstractCollection并 implementsList提供列表的骨架实现。ArrayList具体类 extendsAbstractList并 implementsList、RandomAccess等接口。这个层次结构把“契约”“骨架实现”“具体实现”三层职责分得清清楚楚。你在设计自己的类库时也可以参考这个模式接口定义契约抽象类提供骨架具体类完成最终实现。5. 实操中容易踩的坑与排查技巧5.1 方法重写时的访问权限陷阱继承场景下重写父类方法时子类方法的访问权限不能比父类更严格。父类是protected子类可以是protected或public但不能是private或包级私有。这个规则在 implements 接口时同样适用——接口方法默认是public的所以实现类的方法必须是public写成protected直接编译报错。我见过不少新手在这里卡住报错信息是“attempting to assign weaker access privileges”翻译过来就是“试图分配更弱的访问权限”。看到这个报错第一反应就是检查方法签名前面的修饰符是不是比父类/接口更严格了。5.2 接口默认方法带来的菱形冲突Java 8 引入 default 方法之后接口也能带方法实现了。这就带来一个新问题如果一个类 implements 了两个接口而这两个接口有签名相同的 default 方法编译器会报错要求你显式重写这个方法来解决冲突。interface A { default void hello() { System.out.println(A); } } interface B { default void hello() { System.out.println(B); } } class C implements A, B { // 编译报错class C inherits unrelated defaults for hello() from A and B }解决办法是在 C 里重写 hello并用A.super.hello()或B.super.hello()显式指定调用哪个接口的默认实现。这个坑在引入多个第三方库时特别容易遇到因为不同库可能定义了同名 default 方法的接口。5.3 构造器与初始化顺序的隐蔽问题继承场景下创建子类对象时父类构造器会先执行然后才执行子类构造器。如果父类构造器里调用了某个被子类重写的方法就会出现“子类方法在子类字段初始化之前被调用”的诡异现象导致子类字段读到的是默认值null 或 0而不是你期望的初始值。class Parent { Parent() { print(); } void print() { System.out.println(parent); } } class Child extends Parent { int value 42; Child() { super(); } Override void print() { System.out.println(child value value); } } // new Child() 会输出 child value0而不是 42这个坑非常隐蔽因为代码看起来完全正常。规避方法很简单不要在构造器里调用可被重写的方法。如果确实需要把方法声明为final或private。5.4 常见问题速查表报错/现象可能原因解决思路weaker access privileges重写方法权限比父类/接口严格把修饰符改成 public 或与父类一致inherits unrelated defaults两个接口有同名 default 方法显式重写并用 Xxx.super 指定子类字段读到默认值父类构造器调用了重写方法构造器内避免调用可重写方法cannot inherit from final试图继承 final 类改用组合或实现接口类声明顺序报错implements 写在 extends 前面调整顺序extends 必须在前接口字段修改报错试图给接口字段赋值接口字段是 final 常量不可修改6. 从真实项目案例看选型决策6.1 一个支付模块的重构过程早些年我参与过一个电商后台的支付模块。最初的设计是PaymentService是一个具体类里面用 if-else 判断支付渠道然后调用对应的逻辑。后来要接入新的支付渠道每次都要改这个类改完还要全量回归测试风险很大。重构的第一步是把PaymentService改成接口定义pay()、refund()、query()三个方法。然后每个渠道一个实现类ChannelAPaymentService、ChannelBPaymentService等等。业务代码只依赖PaymentService接口通过工厂或依赖注入拿到具体实现。重构之后新增渠道只需要新增一个实现类业务代码零改动。这就是 implements 的核心价值——把变化点隔离在实现类里让调用方保持稳定。6.2 一个模板方法模式的应用另一个场景是报表导出。不同报表的导出流程大同小异查询数据、格式化、写文件、发通知。差异只在“查询数据”和“格式化”两步。这种场景就适合用抽象类 extends。我定义了一个AbstractReportExporter抽象类把export()方法写成模板方法固定了流程骨架把queryData()和format()声明为抽象方法留给子类实现。具体报表类 extends 这个抽象类只需要实现两个抽象方法即可。这里为什么用 extends 而不是 implements因为子类需要复用父类里export()方法的流程代码和sendNotification()的具体实现。如果只用接口这些公共代码就没地方放每个实现类都得重复写一遍。需要复用实现时用抽象类只需要定义契约时用接口这是选型的核心分界线。6.3 两者结合的综合案例实际项目里最常见的还是两者结合。比如一个消息推送系统// 能力契约可重试 interface Retryable { void retry(); } // 能力契约可序列化发送 interface Sendable { void send(String content); } // 抽象骨架提供公共的日志、统计逻辑 abstract class AbstractSender { protected void log(String msg) { /* 公共日志逻辑 */ } protected void stat(String event) { /* 公共统计逻辑 */ } } // 具体实现继承骨架实现两个能力接口 class SmsSender extends AbstractSender implements Sendable, Retryable { Override public void send(String content) { log(sending sms); // 具体发送逻辑 } Override public void retry() { // 具体重试逻辑 } }这个结构里AbstractSender负责复用Sendable和Retryable负责契约SmsSender负责具体实现。三层职责清晰扩展性和复用性都兼顾了。这种模式在框架设计中非常常见你去看 Spring 的源码到处都是这种结构。7. 几个容易被面试官追问的细节7.1 接口里的字段到底是什么接口里声明的字段不管你有没有写修饰符编译器都会自动加上public static final。也就是说interface Config { int MAX 100; // 等价于 public static final int MAX 100; }这意味着接口字段是常量不能修改而且可以通过Config.MAX直接访问不需要实例。很多人误以为 implements 一个接口就“继承”了它的字段其实只是能直接访问这个常量而已字段本身属于接口不属于实现类。7.2 抽象类能否 implements 接口但不实现方法可以。抽象类 implements 一个接口后可以选择不实现接口里的抽象方法把这些方法留给具体的子类去实现。这是抽象类作为“骨架实现”的常见用法。比如AbstractListimplementsList但没有实现所有方法把get(int)和size()留给了子类。7.3 接口能否 extends 类不能。接口只能 extends 接口不能 extends 类。这是 Java 语言层面的硬规定。接口是纯契约不能继承类的状态字段和行为方法实现所以不允许 extends 类。7.4 一个类能否同时 extends 和 implements 同一个类型不能。如果一个类已经 extends 了某个类就不能再 implements 这个类因为类不是接口。反过来如果一个类 implements 了某个接口也不能再 extends 这个接口因为接口不是类。这个问题的本质是类型系统的一致性编译期就会拦住。7.5 方法签名冲突时怎么办如果一个类从父类继承了一个方法又从接口获得了一个签名相同的方法会怎样答案是父类的方法优先。如果父类的方法实现满足接口的契约就不需要额外做什么。如果父类的方法是抽象的那子类必须实现它。如果父类方法和接口方法的返回类型不兼容编译器会报错。这个规则在《Java 语言规范》里有明确定义实际写代码时遇到的情况不多但面试时偶尔会被问到。8. 我个人的选型口诀与经验总结写了这么多年 Java我总结了一个简单的判断口诀基本能覆盖日常开发 95% 的场景先问关系再问复用最后问数量。先问关系新类型和已有类型是“是一种”还是“能做什么”是“是一种”考虑 extends是“能做什么”考虑 implements。再问复用我是否需要复用已有类型的代码实现需要复用实现且关系是 is-a用 extends 抽象类只需要契约不需要复用用 implements 接口。最后问数量我是否需要从多个来源获得能力需要多个来源只能用 implements 多个接口只需要一个来源extends 和 implements 都可以考虑。还有一个经验对外暴露的 API 尽量用接口内部实现类尽量用抽象类做骨架。这样调用方依赖稳定实现方复用充分。这个原则在写类库和框架时尤其重要写业务代码时也可以参考但不必教条。最后分享一个我踩过的坑曾经有个类同时 extends 了一个父类并 implements 了一个接口父类和接口里都有一个同名方法签名完全一样。我当时以为会冲突结果编译通过了运行也正常。后来查了规范才知道父类的方法自动满足了接口的契约不需要额外处理。但这种情况可读性很差后来我还是把方法在子类里显式重写了一遍加上注释说明避免后来人困惑。代码是写给人看的能显式表达意图的地方尽量不要依赖编译器的隐式规则。