• 第六章《类的高级特性》第5节:接口


    在Java语言中,一个类只能继承一个父类,专业上把这种继承机制称为“单继承”。单继承是一种较为稳妥继承机制,能够规避很多潜在的问题,但这种继承机制的局限性也显而易见:子类不能从多个父类中继承属性和方法,从而无法最大程度的减少重复劳动。为了弥补这个缺陷,Java语言引入了“接口”的概念。接口本质上是一种特殊的类,它最明显的特点就是具有多继承性。

    6.5.1接口的定义

    表示接口的关键字是interface。在IDEA中创建一个接口与创建一个类的操作步骤很相似,只需要从File菜单或右键菜单中选择“New”子菜单,然后在菜单项中单击“Java Class”菜单项,在弹出的对话框中选择“Interface”并填写接口名称即可,如图6-14所示。

    图6-14 创建接口

    在图6-14所示对话框的文本框中填写好接口的名称,然后按下回车键即可完成接口的创建。接口的名称与类的名称有相同的命名习惯,都是以大写字母开头。

    6.5.2接口的特点及用途

    本节一开始就说接口本质上是一种特殊的类,既然它的本质是类,那么程序员当然可以在其中定义属性和方法。但接口到底有什么特殊之处呢?它的第一个特殊之处就是:在接口中定义属性时必须为属性赋值,否则将会出现语法错误,如图6-15所示。

    6-15 接口属性语法错误

    为什么在定义属性时没有为它赋值就会出现语法错误呢?就是因为:接口中的属性会自带三个关键字,分别是:public、static和final。前文讲过:被final关键字修饰的属性一旦被赋值将无法更改。类的属性在定义时都会被赋予默认值,但这个默认值很可能不是程序员想要设置的值。为了让程序员抓住这唯一给属性赋值的机会,编译器要求必须在定义属性时就为其赋值,否则再无赋值机会,这就是为什么在接口中定义属性时必须完成赋值的原因。另外,接口属性自带的static关键字使得接口的所有属性均为静态属性,而public关键字则不允许把属性的访问度设置为protected或private。public、static和final这三个关键字是每个接口属性自带的。如果程序员手动为属性添加上这三个关键字也不会出现语法错误,而只添加其中部分关键字的情况下,编译器会自动补全剩余关键字。

    接口的第二个特殊之处是:在接口中定义抽象方法无需添加abstract关键字,反而在定义普通方法的时候需要额外添加一个default关键字,如图6-16所示。

    图6-16 在接口中定义方法

    为什么在接口中定义方法与在类中定义方法所使用的关键字会不一样呢?在JDK1.8之前的版本,接口中仅能定义抽象方法,也就是说:当时接口中所有的方法全部都只能是抽象方法。既然接口中每一个方法都是抽象方法,那么就无需再额外添加abstract关键字进行特别声明,读者也可以理解为编译器自动在抽象方法前面加上abstract关键字。当然,abstract也会被自动添加到定义接口的interface关键字之前。从JDK1.8开始,Java语言允许在接口中定义普通方法,但是普通方法和抽象方法前面都没有关键字。为了让编译器能够区分抽象方法和普通方法,Java语言规定:定义在接口中的普通方法需要额外添加default关键字。此外,从JDK1.8开始,Java语言还允许在接口中定义静态方法。定义静态方法时,只需要把普通方法的default关键字替换为static关键字即可。接口中的静态方法可以随意调用接口的每一个属性,因为这些属性全部都是静态属性。在接口中,无论是抽象方法、普通方法还是静态方法,都会被编译器自动添加public关键字,因此不能把这些方法设置为其他访问度。此处还需提醒各位读者:不能用接口创建对象,其原因与抽象类不能创建对象的原因相同,都是因为它们有可能包含抽象方法。

    很多读者都会认为:抽象类可以代替接口,因为它们都可以定义静态属性、抽象方法、普通方法和静态方法,区别无非是接口在定义属性和方法的过程中省略了几个关键字而已。这种观点是错误的,本节一开始提到:接口具有多继承性,也就是说,子类只能继承1个父类,但可以同时继承多个接口,这就是为什么Java语言在有了抽象类的情况下仍然还要引入接口的原因。类对接口的继承,在专业上称为“实现”,所以如果A类继承了I接口,都会被表述为:A类实现了I接口。之所以不用“继承”而改用“实现”这个词,就是因为JDK1.8之前的接口中全部都是抽象方法,类需要去实现这些抽象方法。同样,接口的子类也不被称为“子类”,而是称为“实现类”。

    类在实现接口时并不使用extends关键字,而是使用implements,并且可以一次性实现多个接口,书写格式如下。

    类名 implements 接口1,接口2,...,接口n{

        ...

    }

    一个类在继承了父类的基础上,仍然可以同时实现多个接口,但表示继承的extends关键字必须先出现,书写格式如下。

    类名 extends 父类implements 接口1,接口2,...,接口n{

        ...

    }

    一个类无论实现几个接口,都要实现接口中所有的抽象方法,否则的话这个类只能成为一个抽象类。如果多个接口中重复出现名称、参数和返回值类型都相同的抽象方法,实现类只需要把这些抽象方法实现1次即可,下面的【例06_09】很好的展示了这一特性。

    【例06_09 实现重复的抽象方法】

    I1.java

    1. public interface I1 {
    2.     int a = 1;
    3.     void method1();
    4.     default  void  method2(){
    5.     }
    6. }

    I2.java

    1. public interface I2 {
    2.     void method1();
    3. }

    Impl.java

    1. public class Impl implements I1,I2{
    2.     public void method1() {
    3.         System.out.println("实现method1()方法");
    4.     }
    5. }

    【例06_09】中有两个接口分别是I1和I2,这两个接口中都声明了method1()抽象方法,类Impl同时实现I1和I2。Impl原本继承了两个method1()抽象方法,但这两个抽象方法的名称、参数和返回值类型完全相同,此时Impl只需要把method1()抽象方法实现1次就算是同时实现了I1和I2两个接口。另外还需提醒各位读者:Impl在实现method1()抽象方法时,必须以public作为method1()方法的访问修饰符,否则就是降低了方法的访问度。

    接口之间也可以形成继承个关系,并且这种继承是多继承关系。接口在完成继承时也使用extends关键字,下面的【例06_10】展示了子接口如何同时继承多个父接口。

    【例06_10 接口的多继承】

    IChild.java

    1. public interface IChild extends I1,I2{
    2.     void newMethod();
    3. }

    【例06_10】中,接口IChild同时继承了I1和I2两个接口,这样就从两个父接口中继承到所有的属性、抽象方法和已实现方法,并且IChild还扩展出newMethod()抽象方法。因此,一个类如果实现IChild接口,就必须实现它继承链上所有的抽象方法。也就是说:不仅要实现IChild接口中的newMethod()抽象方法,还必须实现其父接口I1和I2中声明的抽象方法。

    一个类可以同时实现多个接口,还能解决多角度分类问题。我们知道:每一种事物都可以从多个角度对其分类,举例来说:马是一种哺乳动物,从生物学的角度进行划分它应该归为兽类。但马可以拉运货物,以事物的用途角度进行划分,也可以把它归为运输工具类。但Java语言类的单继承性就使得任何对象都只能属于某个单一类型,这必然会导致软件在设计过程中产生一些问题。现在来看一个场景:假如有马、直升飞机和乌鸦三种事物,并且以Horse、Helicopter和Crow这三个类分别代表这三种事物。如果以能否呼吸进行分类,那么马和乌鸦属于同一类,而如果以能否飞行进行分类,则乌鸦和直升飞机属于同一类,如果以能否运输货物进行分类,马与直升飞机又属于同一类。假如有一个Measure类,这个类中有3个方法,其中mBreathe()方法能计算参数对象的呼吸频率,而mFly()方法能够计算参数对象的飞行高度,mCarry()方法能够计算参数对象的运输能力。可以看出,Horse对象和Crow类对象都可以成为mBreathe()方法的参数,Crow类对象和Helicopter类对象都可以成为mFly()方法的参数,而Horse类对象和Helicopter类对象又都可以成为mCarry()方法的参数。在这种情况下,应该如何设定mBreathe()、mFly()和mCarry()这三个方法的参数类型呢?如果把mBreathe()方法的参数类型设置为Horse类,那么Crow类对象就不能成为这个方法的参数。同理,mFly()和mCarry()方法在设定参数类型时也会遇到相同的问题。

    而Java语言引入接口的概念就很好的解决了这个问题。请看下面的图6-17:

    图6-17 用接口实现多角度划分事物

    图6-17中,深色背景的方块表示接口,浅色背景的方块表示类。可以看出,Horse类实现了Breatheable和Carryable接口、Helicopter实现了Flyable和Carryable接口,而Crow类则实现了Breatheable和Flyable接口。这的话,只需要把mBreathe()方法的参数类型设置为Breatheable,那么Horse类和Crow类对象就都可以成为mBreathe()方法的参数。同理,把mFly()方法的参数类型设置为Flyable,就能使Helicopter类和Crow类对象都能成为mFly()方法的参数,而想让Horse类和Helicopter类对象都能够成为mCarry()方法的参数,只需要把方法的参数类型设置为Carryable。通过这个例子可以看出:接口的多继承机制能够实现对事物进行多角度分类,从而能够让软件设计具有更大程度的灵活性。

    多继承性虽然能够给软件开发带来很多好处,但也会带来属性和方法的冲突性问题。这些问题主要体现在三个方面:

    一、属性和静态方法冲突

    有I1和I2两个接口,它们都定义了int型属性a,但各自的a属性值并不相同。如果类Impl同时实现了I1和I2,当出现“Impl.a”时,编译器就无法区分这个a属性到底是指从哪个接口中继承而来的a属性。为了解决这个问题,必须由程序员改用“I1.a”或“I2.a”明确指出属性a的出处。其实,如果I1和I2接口中定义了名称和参数相同的静态方法,则其实现类通过类名调用该静态方法时也会遇到相同的问题,解决办法也同样是要用接口名来指明该静态方法的来源。

    二、已实现方法冲突

    如果I1和I2两个接口都定义了method()方法,并且它们都不是抽象方法。假设这两个method()方法参数和返回值类型也相同。在这种情况下,一旦类Impl同时实现I1和I2,就会导致Impl会因同时继承到两个实现过程不相同的method()方法而产生冲突,并且出现语法错误。解决这个问题的方法是:在Impl类中重写method()方法,这样就使得I1和I2接口的method()方法全部被覆盖,以Impl类中重写后的method()方法为最终版本。

    三、抽象方法冲突

    如果I1和I2两个接口都定义了method()方法,并且这两个方法都是抽象方法。假如这两个抽象的method()方法名称和参数相同,但返回值类型不同,则任何一个类都无法同时实现I1和I2接口,因为实现类在实现接口的method()方法时,无法做到其返回值类型与每一个接口的method()方法都相同。在这种情况下,只能被迫放弃实现其中一个接口。

    本节详细讲解了接口的相关知识,很多读者都会问:接口和抽象类中都能定义抽象方法,那么当需要定义一个抽象方法时,到底应该把它定义在接口当中还是定义在抽象类当中呢?仔细思考就会得出答案:优先选择定义在接口中。因为一个类实现接口的同时,它依然能以其他类作为父类。但反过来,如果这个类继承了抽象类,它就无法再以其他类作为父类了。

    除阅读文章外,各位小伙伴还可以点击这里观看我在本站的视频课程学习Java!

  • 相关阅读:
    Java_数组
    【JS Promise】手写实现 promise.all 和promise.race 方法
    python3使用mutagen进行音频元数据处理
    外设篇:触摸屏
    spring3:引入外部属性文件,bean的作用域以及生命周期,factoryBean,自动装配功能(基于xml)
    Python | Django 为什么要使用 WSGI?
    如何应用Python助你在股票中获利?
    民安智库(专业市场调查公司)开展老人体检消费者调查
    【双碳】Acrel-1000DP分布式光伏并网及数据采集与控制的方式
    js事件循环与macro&micro任务队列-前端面试进阶
  • 原文地址:https://blog.csdn.net/shalimu/article/details/128034926