
Java 为什么用「单继承 多接口」取代 C 的多继承——继承、抽象类、接口全梳理学面向对象时很多同学都会冒出同一个疑问继承有三种姿势——普通类继承、抽象类继承、接口实现为什么要有三种C 一个多继承不就够了吗本文分两章回答这个问题第一章快速梳理继承、抽象类、接口三个概念第二章从C 的菱形继承问题说起讲清 Java「单继承 多接口实现」这套设计的来龙去脉最后给出我对接口作用的两点个人理解。目录一、继承、抽象类、接口快速回顾1.1 继承共性抽取代码复用1.2 抽象类不能实例化的「半成品」1.3 接口公共的行为规范1.4 三种方式速查对比二、为什么会有三种继承方式——从 C 菱形继承说起2.1 C 的多继承与菱形继承问题2.2 Java 的改进单继承斩断菱形继承接口补回多继承能力2.3 为什么接口可以放心地「多」2.4 我对接口作用的两点理解三、总结一、继承、抽象类、接口快速回顾1.1 继承共性抽取代码复用继承inheritance机制面向对象程序设计使代码可以复用的最重要手段它允许程序员在保持原有类特性的基础上进行扩展增加新功能。继承主要解决的问题是共性的抽取实现代码复用。比如狗和猫都是动物就可以把共性抽取到父类中// 父类AnimalclassAnimal{publicStringname;publicintage;publicvoideat(){System.out.println(name正在吃饭);}publicvoidsleep(){System.out.println(name正在睡觉);}}// 子类Dog —— 只需关心自己新增的成员classDogextendsAnimal{publicvoidbark(){System.out.println(name正在汪汪叫);}}// 子类CatclassCatextendsAnimal{publicvoidmew(){System.out.println(name正在喵喵叫);}}要点语法class 子类 extends 父类 { ... }Animal称为父类/基类/超类Dog称为子类/派生类子类会将父类中的成员变量和成员方法都继承下来实现时只需关心自己新增的成员继承表达的是is-a 语义狗是一种动物。1.2 抽象类不能实例化的「半成品」在打印图形的例子中父类Shape的draw方法好像并没有什么实际工作主要绘制逻辑都由各种子类完成。像这种没有实际工作的方法可以设计成抽象方法包含抽象方法的类称为抽象类。// 抽象类被 abstract 修饰的类publicabstractclassShape{// 抽象方法被 abstract 修饰没有方法体abstractpublicvoiddraw();abstractvoidcalcArea();// 抽象类也是类可以包含普通方法和属性甚至构造方法publicdoublegetArea(){returnarea;}protecteddoublearea;// 面积}// 矩形类继承抽象类必须重写所有抽象方法publicclassRectextendsShape{privatedoublelength;privatedoublewidth;Overridepublicvoiddraw(){System.out.println(矩形: length length width width);}OverridepublicvoidcalcArea(){arealength*width;}}抽象类核心特性特性说明不能直接实例化new Shape()编译报错Shape 是抽象的无法实例化抽象方法不能是 private抽象方法就是给子类重写的private 无意义非法修饰符组合抽象方法不能被 final / static 修饰因为抽象方法必须被子类重写而 final 和 static 都阻碍重写必须被继承子类要重写父类中的全部抽象方法否则子类也必须是抽象类可以有普通方法和属性抽象类也是类甚至可以有构造方法供子类初始化父类成员抽象类的作用是什么普通类也能被继承、方法也能被重写为什么非要用抽象类因为使用抽象类相当于多了一重编译器的校验。实际工作不应该由父类完成而应由子类完成——如果不小心误用成父类对象普通类编译器不会报错但父类是抽象类就会在实例化时直接提示错误让我们尽早发现问题。这和final的设计初衷类似很多语法存在的意义都是为了「预防出错」充分利用编译器的校验在实际开发中非常有意义。1.3 接口公共的行为规范现实生活中接口的例子比比皆是笔记本上的USB 口可以插 U 盘、鼠标、键盘……所有符合 USB 协议的设备电源插座可以插电脑、电视机、电饭煲……所有符合规范的设备。接口就是公共的行为规范标准。在 Java 中接口可以看成是多个类的公共规范是一种引用数据类型。// USB 接口publicinterfaceUSB{voidopenDevice();// 接口中的方法默认都是 public abstractvoidcloseDevice();}// 鼠标类实现 USB 接口publicclassMouseimplementsUSB{OverridepublicvoidopenDevice(){System.out.println(打开鼠标);}OverridepublicvoidcloseDevice(){System.out.println(关闭鼠标);}publicvoidclick(){System.out.println(鼠标点击);}}接口核心特性特性说明不能 new接口是引用类型但不能直接实例化接口对象方法隐式为public abstract只能是 public abstract其他修饰符如 private都会报错方法不能在接口中实现只能由实现类来实现JDK 8 起额外允许 default 方法变量隐式为public static final接口中的变量都是静态常量可通过接口名直接访问不可修改不能有静态代码块和构造方法—类实现接口用implements子类和父类之间是extends 继承类与接口之间是implements 实现没实现全部抽象方法 → 必须设为抽象类—1.4 三种方式速查对比维度普通类继承抽象类继承接口实现关键字extendsextendsimplements父类能否实例化✅ 可以❌ 不可以❌ 不可以父类能否有普通方法和实例字段✅✅可被子类直接使用❌ 方法全是抽象的变量全是静态常量子类是否必须重写不强制必须重写全部抽象方法必须实现全部抽象方法数量限制单继承单继承可实现多个接口表达语义is-ais-a但不完整的模板具有 xxx 特性can-do二、为什么会有三种继承方式——从 C 菱形继承说起看到这里你可能会问普通类继承不就够了吗抽象类好歹还能带成员和方法实现接口什么都不能带为什么还要它这就要从 C 说起了。2.1 C 的多继承与菱形继承问题C 是支持多继承的——一个类可以同时继承多个父类。表面上这表达能力很强但当继承图形成「菱形」时麻烦就来了A ← 最顶层的基类比如都有一个 name 成员 / \ B C ← B、C 分别继承 A \ / D ← D 同时继承 B 和 C此时 D 的对象中会包含两份 A 的成员从 B 继承来一份、从 C 继承来一份。访问d.name时编译器不知道你用的是哪一份——这就是二义性问题。C 用**虚继承virtual inheritance**来缓解但虚继承语法复杂、实现代价高是 C 公认的难点。菱形继承的详细原理、虚继承的内存布局等内容不在本文展开详细了解可以看我的另一篇博客占位此处放你 C 菱形继承文章的链接。问题的根源在于多个父类都带有实例成员和方法实现多继承时这些「状态」和「实现」在子类中发生重叠冲突。Java 的设计者正是针对这个根源做出了取舍。2.2 Java 的改进单继承斩断菱形继承接口补回多继承能力针对菱形继承问题Java 采取了釜底抽薪的办法类和类之间是单继承的一个类只能有一个直接父类。只有一个父类继承图天然是一条「链」永远不会分叉出「两个都带着 A 成员的父类」二义性从语言规则上就被消灭了——菱形继承问题在 Java 里根本不会发生。但「一刀切」也带来了新问题多继承的能力没有了。现实中「多重身份」的场景非常常见——青蛙既会跑又会游泳鸭子更是水陆空三栖。如果只能继承一个父类这些组合能力就很难表达难免带来不便难道要为每一种能力组合都造一个父类那继承体系只会越来越庞大混乱。为了解决这个问题Java 又引入了接口一个类虽然只能继承一个父类但可以实现多个接口而且接口与接口之间还可以多继承。// 一组接口分别表示「会飞的」「会跑的」「会游泳的」interfaceIFlying{voidfly();}interfaceIRunning{voidrun();}interfaceISwimming{voidswim();}classAnimal{protectedStringname;publicAnimal(Stringname){this.namename;}}// 鸭子水陆空三栖 —— 单继承 多接口实现classDuckextendsAnimalimplementsIRunning,ISwimming,IFlying{publicDuck(Stringname){super(name);}Overridepublicvoidfly(){System.out.println(this.name正在用翅膀飞);}Overridepublicvoidrun(){System.out.println(this.name正在用两条腿跑);}Overridepublicvoidswim(){System.out.println(this.name正在漂在水上);}}同时接口与接口之间可以多继承相当于把多个接口合并在一起用接口达到了「多继承」的目的// 两栖的动物既能跑也能游 —— 接口多继承interfaceIAmphibiousextendsIRunning,ISwimming{}classFrogimplementsIAmphibious{// Frog 需要同时实现 run() 和 swim()}于是 Java 的世界里形成了明确的分工单继承extends 一个父类表达is-a猫是一种动物多实现implements 多个接口表达具有 xxx 特性鸭子既能跑、也能游、还能飞。表达能力没有损失C 用多继承表达的「多重身份」Java 用「单继承 多接口」同样表达了出来还顺便从根源上躲开了菱形继承。2.3 为什么接口可以放心地「多」关键问题来了同样是「多」为什么类多继承会出菱形问题接口多实现却不会回看 2.1 的结论菱形继承的根源是多个父类都带有实例成员和方法实现。而接口恰恰把这两样东西都「抽干」了成员种类普通类 / 抽象类接口实例字段对象状态✅ 有多继承会产生两份状态❌ 没有——变量隐式为public static final是接口自己的常量不随对象分配方法实现体✅ 有多继承会产生两份实现二义性❌ 没有——方法隐式为public abstract没有实现体构造方法 / 静态代码块✅ 有❌ 不允许存在所以当一个类实现多个接口时不存在「两份实现」接口方法全是抽象方法最终的方法体只有一份——由实现类自己写出来多个接口即使声明了同名方法也只是在「要求」同一个方法毫无冲突不存在「两份状态」接口变量全是public static final常量属于接口本身而非对象多个接口之间顶多是常量同名编译器直接报错要求处理不会出现对象数据二义性。一句话接口把「状态」和「实现」都剥掉了只留下纯粹的「行为规范」所以它可以放心地多实现——这就是 Java 用「单继承 多接口」从根本上规避菱形继承的原理。延伸讨论一继承了「已实现该接口」的类方法用谁的一个更刁钻的情况类Son自己实现了接口USB但它继承的父类Father也实现了USB——那openDevice到底算谁的interfaceUSB{voidopenDevice();}// 父类实现了 USB 接口classFatherimplementsUSB{OverridepublicvoidopenDevice(){System.out.println(Father.openDevice);}}// 子类继承 Father同时也声明实现 USBclassSonextendsFatherimplementsUSB{// 什么都没写编译通过}答案不存在二义性因为「实现」从头到尾只有一份。Son什么都不写也完全合法父类Father已经给出了openDevice的实现体Son通过继承拿到了它——接口USB的抽象方法已经被间接满足编译器认可调用new Son().openDevice()时走动态绑定找到对象中实际存在的那份实现也就是从Father继承来的那份如果Son自己重写了openDevice那就调用Son自己的——就近原则永远用对象身上最具体的那个实现。也就是说接口在这里只是再次「确认」Son具备openDevice能力真正的方法体要么来自父类、要么来自自己永远不会出现两个实现打架。延伸讨论二两个接口的同名常量访问的是哪个值方法好说那成员变量呢假设一个类实现的两个接口里各有一个同名但值不同的常量interfaceA{intNUM10;// 隐式为 public static final}interfaceB{intNUM20;// 同名常量值不同}classCimplementsA,B{voidtest(){System.out.println(NUM);// ❌ 编译报错}}编译报错reference to NUM is ambiguous对 NUM 的引用不明确注意Java 不会替你「选一个」——既不会默认用先声明的A.NUM也不会用B.NUM而是直接编译报错逼你写清楚。想消除歧义有两种处理方式// 方式一用接口名限定要谁的值就写谁的前缀System.out.println(A.NUM);// 10System.out.println(B.NUM);// 20// 方式二在类中自己声明一个同名常量把接口的都「屏蔽」掉classCimplementsA,B{publicstaticfinalintNUM30;// 类自己的声明voidtest(){System.out.println(NUM);// ✅ 输出 30类中声明的字段优先于接口继承来的}}另外补充一点class C implements A, B { }本身是合法的——只有当你真正使用到这个有歧义的名字时编译器才会介入报错不使用就没有歧义可言。两个延伸讨论殊途同归再次验证了本节的结论方法的「实现」永远只有一份继承来的或自己写的常量的「冲突」编译器逼你表态限定访问或重新声明——无论怎么「多」Java 都不会出现「不知道该用哪个」的运行期二义性这正是它敢放开接口多实现的底气。严谨性补充JDK 8 之后接口允许default方法带实现体若两个接口的同名 default 方法发生冲突实现类必须重写该方法来消除歧义——Java 用「强制重写」堵住了这个口子依然是规范说了算而不是默选某一份实现。本文讨论的仍是课件所讲的经典规则接口方法默认都是 public abstract。2.4 我对接口作用的两点理解基于上面的分析谈谈我个人对「接口到底为什么存在」的理解。理解一解决菱形继承问题同时也解决了 Java 单继承的问题。对内语言设计层面接口剥掉了状态与实现让「多继承」变得安全——从根源上规避了 C 菱形继承的二义性问题对外表达能力层面Java 只允许单继承表面上比 C 的表达能力弱但接口的多实现 接口间的多继承把这份表达能力原封不动地补了回来。一个类继承一个父类is-a同时实现任意多个接口can-do——「水陆空三栖的鸭子」就是最好的例子。单继承是规则多接口是补偿两者合起来才是 Java 完整的设计。理解二规范化接口统一接口设计。接口的本质是公共的行为规范标准——就像 USB 口定义了协议所有符合规范的设备都能插上来用对实现者implements是一纸契约强迫你实现全部抽象方法代码不规范编译器直接不让过和抽象类一样多了一重编译器校验对使用者只依赖接口、不依赖具体类型。比如一个「散步」方法publicstaticvoidwalk(IRunningrunning){System.out.println(我带着伙伴去散步);running.run();}walk内部根本不关心传进来的到底是猫、青蛙还是机器人只要它implements IRunning会跑就行——甚至参数可以完全不是动物classRobotimplementsIRunning{privateStringname;publicRobot(Stringname){this.namename;}Overridepublicvoidrun(){System.out.println(this.name正在用轮子跑);}}walk(newCat(小猫));// 小猫正在用四条腿跑walk(newFrog(小青蛙));// 小青蛙正在往前跳walk(newRobot(机器人));// 机器人正在用轮子跑再比如给对象数组排序Arrays.sort之所以能给任意对象排序是因为Comparable接口统一规定了compareTo这套比较规范——实现它你的类就自动获得了「可被标准库排序」的能力。有了接口类使用者就不必关注具体类型而只关注某个类是否具备某种能力。在大型工程和团队协作中接口就是模块之间解耦的「契约」接口先行各干各的最后无缝对接。三、总结问题答案三种继承方式分别是什么普通类继承复用 扩展、抽象类继承模板 编译器校验、接口实现行为规范 多重能力C 的问题是什么多继承产生菱形继承子类包含两份基类成员访问二义性问题根源是什么多个父类都带有实例成员和方法实现Java 怎么改的类单继承杜绝菱形接口多实现/接口间多继承补回表达能力为什么接口能多接口方法全是public abstract无实现体、变量全是public static final无实例状态、没有构造方法——没有可冲突的东西最后用三句话收束全文继承解决「代码复用」表达 is-a抽象类解决「父类不该被实例化」且必须让子类重写方法用编译器校验预防出错接口解决「一个类需要多重身份 行为需要统一规范」——它剥离了状态与实现让 Java 用「单继承 多接口」既躲开了 C 的菱形继承又补足了单继承的表达力还顺手定义了大型工程里的「协作契约」。这就是 Java 类型体系中最优雅的一处设计。其实最后总的来说抽象类接口等都是为了更好的复用代码更好的继承。如果继承后类中含有抽象方法就是抽象类如果继承后类中所有方法全部都是抽象方法就是接口不包含实例成员变量。如果这篇文章对你有帮助欢迎点赞 收藏 ⭐ 关注后续会继续更新 Java 面向对象系列文章