欢迎光临
我们一直在努力

接口 —— 从 其限制 到 间接多继承 蕴含设计哲学的精妙之处

文章目录

  • 例子·引出
  • 语法规则
    • 接口的“限制”
  • 接口的实现 —— “多继承”
  • 接口间的”继承“ ——> 拓展
  • Java禁止直接继承的深层原因

    • 为什么会禁止
    • 解决的方法
    • 为什么C++和python等语言支持 直接多继承
    • 为什么Java不去解决菱形问题,而是避开,用接口
  • 【接口】设计的精妙之处

    • Java的状态问题:
    • 运行时性能与 JVM 实现的简洁性
    • “组合优于继承”设计模式的深层原因

例子·引出

【接口】顾名思义,某个功能提供 一个接口,供外界使用

恰如,现实生活中,接口的例子比比皆是,比如:笔记本上的USB口,电源插座等 电脑的USB口上,可以插:U盘、鼠标、键盘…所有符合USB协议的设备; 电源插座插孔上,可以插:电脑、电视机、电饭煲…所有符合规范的设备

可以知道的是: 接口就是公共的行为规范标准,在实现时,只要符合规范标准,就可以通用。

在Java中,接口可以看成是:

多个类的公共规范,是一种引用数据类型

语法规则

public interface 接口名称{

//变量
public static final int a = 10;
int b = 10; //public static final 在接口中定义变量时为硬性搭配,不加上后续会自动加上

// 方法 —— 默认为抽象方法
public abstract void method1();
public void method2();
abstract void method3();
void method4(); // public abstract 是固定搭配,可以不写, 不写编译器也会自动加上。 因此method 1~4 创建方式最终都是一样的
}

创建接口的 软性 要求

  • 创建接口时, 接口的命名一般以大写字母 I 开头
  • 接口的命名一般使用 “形容词” 词性的单词.
  • 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性(某些公司有自己的命名规则,来保持简洁性)
  • 接口的“限制”

    上方的语法规则代码中

    • 在定义变量方面可知的是: 接口中,定义的变量只能为 “常量”,即只能被“public static final”

      常量? 即在创建时就得赋值,且后续不能修改。 所以创建变量时,“public static final”可省略不敲,但如果不在当时给予赋值 就会报错

      创建得变量中,还赋予”static“修饰,这就代表了接口中得变量为 “全局变量” 即有static的特性 与抽象类中的变量就不同了,可明确的是:在定义方式上,抽象类与普通类无任何区别(即支持各种修饰符),而 接口中 就固定且不变且强制的 只能被”public static final“修饰

    • 在定义方法有相同 也似有不同 大白话来说,普通类创建方法的方式,抽象类也可一样实现,不同的就是抽象类可以 创建抽象方法 对于接口来说,默认被”public abstract“修饰,即是抽象方法,和变量不同的是,在Java 8以后,可以手动修改方法前的修饰(可以单独被”static“,"default"修饰,且只有这两个可选修饰,其它的都不行)

      即接口中,方法 基本 只能是抽象方法(无具体实现),在早期Java中,对于接口就是以无具体实现 为目的,在后来,为了让接口内容无缺失,就有了让其方法有具体实现的方式,即被”static“,"default"修饰,这样默认的“public abstract”就不会加以修饰,不成为抽象方法,以普通方法就可有具体实现内容

    通过以上分析,接口的限制似乎”过多“,但其限制决定着接口的作用

    —— 接口是为了扩展能力(让一个类能做更多事)

    掰开揉碎: 在整个项目中,设计抽象类,其目的就是成为父类,其便利点就在于 抽象方法中可省去无意义的实现,让子类去实现。这样看来对后续 继承多态确实是方便极简了,但对于抽象类本身和继承的某个方面 还存在方便性的不足 —— 多继承

    如果要设计多个抽象类,如下

    //鸟类
    public class bird extends Animal{ //继承父类(父类为抽象类)
    @override //重写父类的抽象方法
    public void eat(){
    System.out.println("吃鸟粮");
    }
    //……重写父类其他的抽象方法

    //设计子类 特定 的方法
    public void fiy(){
    System.out.println("鸟飞起来");
    }
    //……
    }
    //蜻蜓类
    public class dragonfly extends Animal{
    @override //重写父类的抽象方法
    public void eat(){
    System.out.println("吃蜻蜓粮");
    }
    //……重写父类其他的抽象方法

    //子类 特定 的方法
    public void fiy(){
    System.out.println("蜻蜓飞起来");
    }
    //……
    }
    //狗类
    public class dog extends Animal{
    @override //重写父类的抽象方法
    public void eat(){
    System.out.println("吃狗粮");
    }
    //……重写父类其他的抽象方法

    //子类特定的方法
    public void run(){
    System.out.println("狗跑起来了");
    }
    ……
    }
    //机器人类
    public class robot{ //机器人不是动物,不能继承Animal类
    //……其他细节
    public void run(){
    System.out.println("机器人跑起来了");
    }

    }

    可以总结的是:接口的限制还体现在 初始化方面,由于接口的变量只能是常量,所以不存在对变量进行初始化,即接口不存在 构造方法,各种代码块 !

    上方代码可分析的是:鸟类特定的方法是fly, 蜻蜓特定的方法是fly, 狗类特定的方法是run, 机器人类也有方法run

    由于父类Animal需要做到动物的共性,恰如 吃 可以在父类中编写,但 飞和跑 就不是共性,”飞“肯定不止鸟和蜻蜓,还有某些昆虫等许多,此时就要在这多个类中一个个复制”飞“这个方法,如果不止”飞“还有其他更多方法需要”复制“,时间成本变多的同时 忘记 搞混 等错误就会发生

    不是说,重写不也是复制吗,不也会忘记,搞混吗? 此时便体现了父类为抽象类的好处了,只要需要重写的方法设计为抽象方法,就不会出现忘记,搞混的情况,而且在子类中 通过快捷键便可快速生成那些需要重写的方法,只需编写且内容就可。

    既然有些特定的方法多个子类中有,但也不是全部子类中有,此时便有个想法:把那些子类中出现频率高的方法,重新设计一个抽象类,把那些方法搞进去,让那些子类除了继承父类,再继承这个抽象类。 即是继承两个父类

    这种情况是典型的多继承,在C++,Python中,都是支持多继承的,但在Java中不支持多继承,为什么(后续分析),但对于多继承,在某些项目中又有许多需求,因此,Java不能舍弃 多继承 的需求,但可以 间·接 来实现多继承继承的效果,通过某种方式 —— 接口

    一个类使用接口,就是类“继承”接口,此时并不称为“继承”,叫“实现”, 即类实现接口 “继承”指的是类与类之间,”实现“指的是类与接口之间 "继承"和”实现“只是说法不同,如何调用等其他的基本一样

    可以总结的是:

    抽象类相较于普通类,是代码的精简(抽象方法无实现内容),基本无另外限制,且作为父类,代码复用程度高。 而接口限制很大(是为了某些功能的拓展),决定着其作用是为了 多个类 功能的拓展,既然是”拓展“,就可多”拓展“,即是类可多实现接口,便是间接 多继承!

    普通类 ——> 抽象类 ——> 接口 辨析清楚便会发现,不难,喵的,理解起来越来越抽象

    接口的实现 —— “多继承”

    一个类实现 一个及多个 接口,都是对该类功能的拓展,如果一个类所需拓展功能较多,实现多个接口便完美解决了 Java不支持多继承的不足

    class Animal {
    protected String name;

    public Animal(String name) {
    this.name = name;
    }
    }

    //提供多组接口,是对动物某些特性的拓展
    interface IFlying {
    void fly();
    }

    interface IRunning {
    void run();
    }

    interface ISwimming {
    void swim();
    }

    class Cat extends Animal implements IRunning {
    public Cat(String name) {
    super(name);
    }

    @Override
    public void run() {
    System.out.println(this.name + "正在用四条腿跑");
    }
    }

    class Fish extends Animal implements ISwimming {
    public Fish(String name) {
    super(name);
    }

    @Override
    public void swim() {
    System.out.println(this.name + "正在用尾巴游泳");
    }
    }

    //青蛙, 既能跑, 又能游(两栖动物)
    class Frog extends Animal implements IRunning, ISwimming {
    public Frog(String name) {
    super(name);
    }

    @Override
    public void run() {
    System.out.println(this.name + "正在往前跳");
    }

    @Override
    public void swim() {
    System.out.println(this.name + "正在蹬腿游泳");
    }
    }

    以上代码可以知道的是:一个类继承父类后,还可实现接口(一个甚至多个) 这里有个硬性规定是:如果一个类有父类和接口,在声明时,必须先继承父类,再实现接口,不能反过来,否则保错 以上代码有个麻烦的点在于:如果一个类中要实现多个接口,像上面的青蛙类有两个接口,需要一个个敲出接口的名称,此时有个快捷键 —— “CTRL + i ” ,可出现全部接口 并选择所需要的接口,即可快速实现接口

    接口间的”继承“ ——> 拓展

    类 之间可以实现继承,接口之间也可继承(称为“拓展”,下面用“继承”说明) 接口之间继承使用的关键字依旧是 extends

    interface IA{
    void testA();
    }

    interface IB{
    void testB();
    }

    interface IC extends IA,IB{
    void testC();
    }

    class test implements IC{
    @Override
    public void testA() {
    //……
    }

    @Override
    public void testB() {
    //……
    }

    @Override
    public void testC() {
    //……
    }
    }

    可分析的是,接口之间可继承且可多继承,普通类继承 子类接口(如IC接口),此时,所有的抽象方法需被重写,否则报错! 接口间的继承相当于把多个接口合并在一起

    Java禁止直接继承的深层原因

    为什么会禁止

    避免一个被称为 “菱形问题”(Diamond Problem) 的二义性难题 !

    假设 Java 允许一个类继承多个父类,就会出现以下情况: 有一个基类 A,它定义了一个 sayHello() 方法。 两个类 B 和 C 都继承自 A,并且都重写了 sayHello() 方法,提供了不同的实现。 现在,如果有一个类 D 想要同时继承 B 和 C。 这就形成了一个 菱形 的继承结构。当 D 的对象调用 sayHello() 方法时,问题就出现了:编译器无法确定应该调用从 B 继承来的版本,还是从 C 继承来的版本。

    这种不确定性会破坏代码的清晰性和可预测性,增加代码维护的复杂度(所以咯,C++语法里就有菱形继承来避免这个问题) 为了避免从根源上出现这种麻烦,Java 的设计者选择了单继承,即一个类只能有一个直接父类

    解决的方法

    可知的是,Java提供了 接口(Interface) 机制来实现类似的功能,同时巧妙地避开了“菱形问题”

    在 Java 8 之前,接口只包含抽象方法(没有方法体),所以实现类必须提供所有方法的实现,不存在调用哪个父类方法的歧义 但前面的内容可以知晓的是:接口可以包含 default 等方法(带有默认实现的方法)。 这是由于 Java 8 之后,支持了可包含其他默认的方法,那么接口中有不是抽象的方法,是否会重新引发“菱形问题”?

    答案是肯定的,不会引发 因为Java接口中制定有明确的规则,来防止些许可能引发的问题

    • 强制重写:如果一个类实现的多个接口中,存在签名相同的 default 方法,编译器会直接报错。
    • 明确指定:必须在类中重写这个有冲突的方法,并可以使用InterfaceName.super.methodName() 的语法来明确指定调用哪个接口的默认实现

    以上的规定,将选择权交给了程序员,要求开发者显式地解决潜在冲突

    可认定的是:Java 的设计哲学是“简单和清晰优于复杂”。通过禁止类的多继承,并从语言层面规避了“菱形问题”,使得代码结构更清晰,更易于理解和维护

    为什么C++和python等语言支持 直接多继承

    • C++ 完全支持多继承,但也因此直接面临“菱形问题”带来的数据冗余和访问二义性

      它的解决方案是使用 virtual 关键字进行虚继承 在菱形继承结构中,最底层的派生类会包含两份顶层基类的成员副本,这不仅浪费内存,还会导致访问时产生歧义 C++的解决方案是在语言逻辑方面,即在中间层类继承顶层基类时,使用 virtual 关键字

      • 虚继承确保了无论通过多少条路径继承,最终的派生类(DogCat)中只会包含一份共享的顶层基类(Animal)实例, 这从根本上消除了数据冗余和访问二义性
    • Python 也完全支持多继承,它通过一套称为 方法解析顺序 (Method Resolution Order, MRO) 的算法来避免“菱形问题”

      方法解析的机制:当一个方法被调用时,Python 会按照一个预先计算好的、确定的顺序来查找该方法。这个顺序就是 MRO Python 使用 C3 线性化算法来计算 MRO。该算法遵循几个核心原则,例如“子类优先于父类”和“遵循从左到右的继承声明顺序” C3 算法保证了 MRO 的唯一性和一致性。在任何继承结构中,每个类都只会在 MRO 列表中出现一次。因此,当调用一个方法时,Python 会严格按照这个线性顺序查找,找到第一个匹配项就执行,从而避免了二义性

    为什么Java不去解决菱形问题,而是避开,用接口

    是其设计哲学及多方面权衡后的决定

    Java 之所以没有支持多接触,并非因为技术无法实现,而是基于一种“工程哲学”的取舍。Java 的设计者(特别是创始者 James Gosling)认为,解决多继承复杂性的代价,远大于多继承带来的便利性

    为何会“复杂”? 因为C++和python支持多继承的代价就是代码编写的复杂,语言学习的难度上升,以及安全方面的多方面因素

    • C++ 的痛点:在 C++ 中,如果你忘记加 virtual 关键字,菱形问题就会出现。这种“隐式”的陷阱要求程序员必须对内存布局有极深的理解。
    • Java 的选择:Java 的设计哲学是“让语言简单到即使是初学者也能写出健壮的代码” Java 宁愿牺牲掉多继承带来的那一点点代码复用便利,也不愿意让程序员在复杂的继承树和内存模型中通过“猜谜”来写代码。

    结论:Java 认为,消除问题的根源(禁止多继承)比解决问题本身(引入复杂的规则)更简单、更安全

    【接口】设计的精妙之处

    Java的状态问题:

    接口(Interface):Java 允许接口的多继承(extends),因为接口在 Java 8 之前只包含方法签名,没有状态(成员变量)。没有状态,就不会有“数据冗余”的问题。

    类(Class):类不仅包含行为,还包含状态(字段)。如果允许类多继承,当两个父类都有 int age 字段时,子类到底该继承哪一个?或者内存里存两份? 如果存两份,对象体积膨胀,且访问时需要复杂的偏移量计算(C++ 的做法)。 如果合并,又回到了 C++ 虚继承的复杂实现。

    Java 的策略:Java 严格区分了“行为”(接口)和“状态”(类)。它允许行为的多种继承(接口),但严格限制状态的单一来源(单继承)。

    运行时性能与 JVM 实现的简洁性

    Java 极其强调运行时的性能和可移植性,多继承会给 JVM 的实现带来巨大的麻烦:

    • 对象布局:在单继承中,对象的内存布局是线性的,访问父类字段非常快 而在多继承中,对象的内存布局会变得支离破碎,JVM 需要维护复杂的“虚基类指针”和偏移量表
    • 方法调用:虽然现代 CPU 很快,但多继承会导致方法查找表(vtable)变得极其复杂 Java 选择单继承,使得 JVM 在进行方法绑定和字节码验证时更加高效、确定

    结论:为了让 JVM 跑得更快、更稳,Java 牺牲了语言层面的多继承特性

    “组合优于继承”设计模式的深层原因

    Java 的设计深受设计模式的影响,其中最著名的一条原则就是:组合优于继承 (Composition over Inheritance)

    多继承往往被滥用来强行耦合代码 Java 鼓励你通过 组合(在一个类中持有另一个类的实例)来实现功能复用,而不是通过继承。

    如果你需要一个“会飞的汽车”,在支持多继承的语言里你可能会写 class FlyingCar extends Car, Airplane 但在 Java 中,推荐的做法是 class Car { private AirplaneBehavior flyBehavior; } 这种方式比多继承更灵活,因为你可以在运行时动态改变行为,而且完全避免了菱形问题。

    虽然 Java 禁止类的多继承,但在 Java 8 中,它通过接口的 默认方法 小心翼翼地引入了“多实现”的能力 为什么敢加? 因为接口依然不能有状态(字段),所以只解决了“行为冲突” 怎么解决冲突? 正如之前提到的,Java 不自动解决冲突,而是强制程序员手动解决(通过重写并指定 Interface.super.method())

    这便也再次 可以看出 Java 的态度了:把控制权交给程序员,而不是让编译器在后台做复杂的运算

    如下表格总结:

    维度C++ / Python 的做法Java 的做法
    核心策略 解决它:引入复杂的算法(虚继承/MRO)来处理冲突 规避它:直接禁止类的多继承,从根源上消灭问题
    语言复杂度 高(需要理解内存布局或解析顺序) 低(规则简单直观)
    运行时开销 较高(对象布局复杂,查找慢) 极低(对象布局紧凑,查找快)
    状态管理 容易混淆(多份副本) 清晰(单一来源)
    赞(0)
    未经允许不得转载:171主机测评 » 接口 —— 从 其限制 到 间接多继承 蕴含设计哲学的精妙之处
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址