Java | interface 和 implements 关键字【接口,看这篇就够了】
学完继承、学完多态,但面对汹涌而来:ocean:的接口,相信很多同学都不知所措,因此我耗费几天几夜的时间,搜寻大量书籍资料,苦心闭关钻研,写出了一篇关于 Java 的接口从入门小白到精通大佬的学习之路,相信这篇文章一定对您有所帮助:book:
@TOC
🌳接口的基本概念引入
Java 接口是一系列方法的声明,是一些方法特征的集合
对于接口,它是 Java 中一个新增的知识点,而 C++中没有,因为 Java 有一个缺陷就是==不可以实现多继承==,只可以单继承,这就限制了有些功能的使用,于是为了让 Java 也能有这种能力,因为提出了接口的概念
对于接口的基本概念,大家应该要回想一下我们上一文所讲的 abstract 抽象类的概念,因为接口它与抽象类非常类似,在抽象类中我们可以了解到其实除了不可以实现一些抽象方法外,其余的和正常的类没有什么本质的区别,一些常量、变量,私有、静态方法都可以定义,但是在接口中,就只能有抽象方法以及常量,而且接口中所有抽象方法的访问权限都是 public 公开的,因为它也算是 static 静态方法,所以可以省略 public 和 static 这两个关键字
但是从 JDK8 开始,就有了一些改变,接口中也可以定义 default 和 private 关键字修饰的方法,对于 default 关键字修饰的方法,不可以将此关键字省略,因为在接口体中不允许定义通常的带方法体的 public 实例方法;而对于 private 修饰的方法,则是配合 default 默认方法进行使用,即将某些算法封装在 private 方法中,供接口中的实例方法调用
🌳如何去定义和实现一个接口
了解了接口的基本概念之后,是不是很想知道一个接口时怎么去定义和实现呢,让我们马上来看一看吧
🔑【interface 关键字】
首先最基本的定义一个接口,对于接口,如果你觉得有一些方法它们有一个功能的类名,那你就可以把它定义为一个接口,在这个接口中去定义一些抽象方法,比方是大家都会运动,运动的方式有很多,比方说跑步、游泳等等,但是你做这些事情的方式的方式和节奏和人家专业运动员又不一样,所以可以由不同的类去继承这个接口,然后去实现具体的对应的功能,
这和类的定义很相似,细心的小伙伴可以看出来这是将 class 关键字换成类 inferface 关键字,但是变得可不止这一种哦,接口可是有它的专属图表的:sparkler:
IDEA
eclipse
这两个 Java 编译器应该是大家用的最用的了,对于接口,细心的小伙伴应该可以发现,存在一个【大写的 I】,这个标志就是【interface】的首字母大写了,相信这点很多人都没有发现吧
🔑【implements 关键字】
好,说完如何入定义一个接口,接下去就来讲讲怎么去实现一个接口吧
那就是用【implements 】这个关键字,通过一个具体的类去实现
但是这样的话就会出现报错,这个我们在继承抽象类的时候就有说过,继承一个抽象类,就要去重写其所有的抽象方法
【注意事项】
对于抽象类的话,如果你用一个抽象类去继承,那么你就不用重写这个抽象方法,当然对于接口也是一样
如果一个非 abstract 类实现了某个接口,那么这个类就必须重写该接口的所有抽象方法
如果一个 abstract 类实现了某个接口,那么这个类可以选择重写接口中的抽象方法或者该接口的抽象方法
🌳接口特点及作用
了解了接口的定义和实现之后,接下来我们来说一说接口有哪些特点以及其具体的作用
🍃接口的特点
①接口虽与抽象类相似,但是比抽象类更加抽象,却不需要写 abstract 关键字,因为接口中所有方法都时抽象的,因此可以省略这个关键字
②接口中只可以有常量,而且都是 public、static、final 关键字修饰的,但是不可以有变量
③接口没有构造方法,因此不可以用 new 关键字去创建接口的对象,而是要用一个具体的类去 implements 实现这个接口
④一个类可以实现多个接口,一个接口可以继承多个接口【有点抽象,上代码】
上述代码实现的是一个类实现多个接口,这个类不是一个抽象类,那就要重写实现接口的中的所有抽象方法
好,具体测试不给出了,主要是看这个接口可以继承多个接口,可以看出 Sport 接口使用了 extends 关键字继承类 Law 和 People 这两个接口
以上的这==两个特点很重要==,也是弥补了 Java 类不能多继承的缺陷
⑤一个类,它可以在继承父类的情况下同时实现接口
📕接口的作用和意义
有些刚刚接触 Java 接口的小伙伴就很疑惑,这个接口到底是用来干嘛的呢,它究竟有什么具体的作用
首先我们来总的概括一下,就是一句话:【定义一个公共规范,实现一个统一访问】
对于公共规范这个概念,就是大家都认可的,是一种标准,就像是 USB(通用串行总线)一样,这个接口是很多家公司联合提出的,因此属于一个规范,在日常生活中,我们使用的很多设备都拥有 USB 接口,这个 USB 接口呢,它就是 Java 中所说的接口,如果有一个物件,比如说笔记本,拥有这个接口,也相当于是实现了这个接口,那就说明笔记本这个类有了 USB 这个功能,外部设备便可以与它产生一个联系,比如说最常见的 U 盘,只要是有 USB 接口的地方,那么这个 U 盘都可以使用,这么说大家应该有点清楚了吧,==下面会更加详细地深入了解接口==
对于统一访问,举个例子,对于 LOL 这款游戏大家应该都玩过,一个英雄,是不是一定会有相同的功能,比如说攻击、点塔、补刀这些,但是 LOL 中 157 个英雄,假设它们都对应一个类,难道在每个英雄类中都去写这三个功能吗,那一定不会,这是就可以定义一个基本英雄功能接口,里面封装了所有英雄所具备的基本能力,然后所有英雄类都去访问这个接口就可以
🌳接口的 UML 图(类图)
初步入门了接口后,接下去我们就要了解接口与它的实现类之间所存在的逻辑框架关系,也就是类图,这可以进一步帮助我们去理解接口
🍃UML 图的基本概念
统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。可以帮助我们简单、统一、图形化、能表达软件设计中的动态与静态信息
🍃UML 图的作用
可以帮助我们清晰勾勒出一个类族的框架接口,继而对此项目整体逻辑接口更加了解
UML 图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型
🍃接口 UML 图的基本结构和组成
对于接口的 UML 图与类的 UML 图很类似,主要是使用一个长方形去描述一个类或接口,将这个长方形垂直地分为 3 层
第一层是名字层,接口的字形必须是斜体字形,而且需要用<>修饰名字,格式为【接口修饰符\n 接口名称】
第二层是常量层,列出接口中的常量及类型,格式为【常量名字:类型】
第三层是方法层,也称操作层,列出接口中的方法及返回类型,格式为【方法名字(参数列表):类型】
🎈继承关系类图与接口图的区别
子类与父类的继承关系所呈现的 UML 类图
接口与实现类所呈现的 UML 类图
相信通过这两张 UML 图的对比分析,你对 UML 类图也有了一个基本的见解了
🌳接口回调与多态的联系
在前面将多态的时候,讲到上转型对象时我有提到过接口回调这个东西,这在接口中是比较重要的,因此做一个区分
📚权威解释
对于向上转型,就是父类引用去引用子类对象
而对于接口回调,就是把实现某一接口的类创建的对象的引用赋值给该接口【声明的接口变量】,那么该接口就可以调用被类实现的接口方法以及接口提供的 default 方法
对于它们二者的区别,还要说到使用接口的核心原因:为了能够向上转型为多个基类型。即利用接口的多实现,可向上转型为多个接口基类型,从实现了某接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的
所以对于==接口回调==,强调使用接口来实现回调对象方法使用权的功能;对于==向上转型==,则牵涉到多态和运行期绑定的范畴以上解释来自《Tinking in Java》这本书
🍃具体案例分析
说了这么多概念,您对接口回调一定还没有一个很清晰的认识,接下去我们通过一个小案例一起来看看
从这个小案例可以看出,对于接口回调,就是将一个实现接口的类所定义的对象的引用给到一个接口所声明的变量,然后上面说了,可以实现回调对象方法使用权的功能,也就是去调用子类重写接口中抽象方法
🌳函数接口与 Lambda 表达式
讲完了接口回调,我们再来说一下函数接口与 Lambda 表达式
🍃Lambda 表达式
这个 Lambda 表达式的话也是 JDK8 新出的,当时出个这个概念的时候备受争议,因为这简直颠覆了大家的想象,都说居然可以这么去优化一个表达式,对此表示非常地惊奇:jack_o_lantern:
接着就让我们先来了解一下这个 Lambda 表达式,了解一下它有什么优缺点
Lambda 表达式,也可称为闭包。类似于 JavaScript 中的闭包,它是推动 Java8 发布的最重要的新特性
优点
1、代码更加简洁
2、减少匿名内部类的创建,节省资源⭐
3、使用时不用去记忆所使用的接口和抽象函数
缺点
1、若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(并行计算有时需要预热才显示出效率优势)
2、不容易调试。
3、若其他程序员没有学过 lambda 表达式,代码不容易让其他语言的程序员看懂⭐
上面提到了一个叫做匿名内部类,这个我还没讲到,放在下一篇文章,大家可以先去了解一下,匿名内部类,这是内部类的一种
🍃函数式接口
然后我们再来了解一下函数式接口
首先必须是接口、其次接口中有且仅有一个抽象方法的形式⭐
通常我们会在接口上加上一个 @FunctionalInterface 注解,标记该接口必须是满足函数式接口⭐
定义方法如下
首先根据上面这个接口,我们去实现一个匿名内部类
对于如何去进行一个简化,我们需要先了解其规则
🍃简化规则定义
参数类型可以省略不写
如果只有一个参数,参数类型可以省略,同时()也可以省略
如果 Lambda 表达式的方法体代码==只有一行代码==。可以省略大括号不写,同时要省略分号!
如果 Lambda 表达式的方法体代码只有一行代码。可以省略大括号不写。此时,如果这行代码是 return 语句,必须省略 return 不写,同时也省略“;”不写
然后我们就对上面的代码进行一个简化
到这里大家可能还是没有看懂,那我们再来多看几个,就能懂了
好,我们来详细地说明一下,对于 Arrays 这个 API 的数组排序,相信大家用的是最多的,默认是升序,这里是进行了一个重写然后令其可以实现降序输出
首先,应该去掉的就是,因为我们只需要这个匿名内部类的形参列表和方法体代码,然后要加上一个【—>】箭头,要注意,这个箭头可不是 C/C++里面的指针 new Comparator() { @Override public int 这段代码
然后根据第一条规则可以知道, 参数类型可以省略不写,所以只留下(o1, o2)
接着就是省略这个 reutrn 和语句后面的分号“;”
最终的简化结果就是 Arrays.sort(ages, (o1, o2) -> o2 - o1);
再来一个有关按钮监听事件 ActionListener 的匿名内部类形式简化
首先是一样,省略这一段代码 new ActionListener() { @Override public void actionPerformed
接着是省略形参值 ActionEvent
最根据第二条规则,如果只有一个参数,参数类型可以省略,同时()也可以省略,省略 e 外的小括号
最终形式便是 btn.addActionListener( e -> System.out.println("登录一下~~~"));
好,又看了两个小案例,这些您对 Lambda 表达式简化匿名内部类有了一个基本的认识了,接下来我们说一些小贴士
💡细心小贴士
如果大家细心的话,对于有些方法,按住 ctrl 键鼠标点进去,就可以看到这是一个函数式接口,如果看到了【@FunctionalInterface】注解,那就表明这个匿名内部类可以使用 Lambda 表达式来简化,点进我们刚才那个 sort()排序的 Comparator 接口,就可以看到这个注解
🌳深入理解接口【面向接口的思维】
了解接口后,我们要开始第二层次,也就是理解接口,首先就是要进行思考,提出相应的问题
❓提问一:为什么不在一个类中直接实现相应的方法,而是要先进行接口抽象呢?
答:这样会造成代码冗余,众多类中都有相同的一个功能,只是调用的对象不同,继而产生内存浪费。这时就可以使用接口去封装这个功能,通过父类引用去接收子类对象,从而实现多态
❓提问二:接口的真正用处在哪里,用接口可以帮助我们实现什么?
答:对于一个接口,上面在讲概念的时候有提到一些,就类似于一个功能库一般,在这个功能库中呢,你可以定义许多别人可能会用的到的功能,这样当别人有需要时,便无需去继承一个抽象类导致类族群混乱,或者自己重写定义一个方法导致增加内存。完全可以把大家都会用得到的功能,并且可以实现多态的功能放入此方法库,在定义这个方法的时候完全无需去考虑它是如何实现的,只需要定义好其标准以及参数的设定
因此可以看出,在一个项目开发时,拥有一些实用的接口是多么重要,既能有一个统一的规范、有一个严格的标准,而且还可以提高开发的效率,减少类族的复杂性,上面也讲到过,接口其实很好地弥补了 Java 无法多继承这个缺陷,当你继承了一个父类,但是又不想再添加一个祖先父类的抽象类,将类族混乱。接口就是一个很好的选择,可以帮助我们实现想要实现的功能
❓提问三:接口与抽象类如此地相似,为什么有的时候要使用接口而不用抽象类呢?
答:对于抽象类,它可以让自己的引用去接收子类对象;对于接口,它可以让自己声明的变量,一样去接收子类对象,它们都可以在获取到子类对象后调用子类重写的抽象方法,继而实现多态。
但是对于抽象类,它始终都是一个类,是需要被继承才能让子类去重写自己所拥有的抽象方法,但是当一些子类继承了一些父类拥有但是自己却不需要的功能时,这时候就会出问题,造成内存浪费。如果当我们仅仅是为了实现多态,而且又是很多类都需要这个功能,却不想要去继承一个父类获取这个方法,就可以将此方法写入接口,通过【implements】这个关键字去实现这个接口,继而在类内重写这个抽象方法来实现多态
🌳接口的实战项目演练
在本小节中,我会设置三个实战项目,从浅入深,层层递进,==帮助大家来更好地理解接口在实际的应用中到底是如何去使用的==
"Hello World"【⭐】
看到这个标题,你不会真的以为只是输出【Helllo World】吧,那是不会的,只是这个案例作为第一个,比较简易一些,帮助大家来回顾上面所学的知识点
上代码
第一个实战项目,作为我们对于上述所讲的接口的基本定义和实现以及接口回调、Lambda 表达式做一个总结
首先,说话是一个能力,将其封装成接口的形式,无论是中国人还是英国人都会说话,因此都去实现了这个接口
然后通过一个普通类去==搭建接口和实现类之间的关系==,将接口所声明的变量作为形参,在主方法中通过调用这个类的方法传入实现类的对象去实现一个接口的回调,继而去展现出多态
然后下面,我又使用了匿名内部类的形式去直接重写了这个接口中的抽象方法,从运行结果可以看出,也能呈现出同样的效果
最后一种,则是对匿名内部类做一个简化操作,这就要使用到我们上面所提到的 Lambda 表达式了,具体规则不细讲,如果有点遗忘的话可以再翻上去看看
新能源时代【⭐⭐⭐】
很夸张的一个标题,突然脑洞打开想到的,哈哈,和项目也是有一些联系:lock:
上代码
这个项目,很明显比上一个小项目稍微复杂了一些,进一步地带大家理解接口,区分接口与抽象类之间的关系,明白接口为什么可以弥补多继承的缺口
首先定义了一个机动车抽象类,里面含有一个刹车方法,对于公交车、出租车都是属于机动车,所以应该让这两个类去继承机动车这个父类,而且他们都可以有【刹车】这一个功能,但是对于电影院类,则不属于机动车,只是一个独立与机动车类族的一个单独类,但是对于【控制温度】和【收取费用】这两个功能电影院也是需要有,可是呢,电影院不会刹车呀,比如放一些爱情片你说怎么刹车,就这么放下去了,管你下面有没有小孩(:dog:)
难道为此再去定义一个抽象类例如多功能电影院吗,这如果实在我们这种小项目是没关系,但如果这个场景放在实际企业开发中,新定义一个抽象类这种行为是具有风险性的,因为随着抽象类的增加,就需要重新构建上层类族之间的关系,就会使得整个项目的框架逻辑变得更加复杂,这种想到增加功能就去多写一个抽象类的行为是不可取的,
因此我们就想到了接口这个东西,我们可以将收取费用和控制空调温度封装到一个功能接口中,或者分开定义也可以。这样的话,影院就不需要去继承机动车类,而是只需要实现这两个接口即可
对于主方法中的一些操作,我都清晰地将每个类分割开来,通过接口所声明的变量去接收实现类的对象,继而去呈现一个接口的回调,从运行结果也可以很清晰地看出
疯狂农场【⭐⭐⭐⭐⭐】
首先声明,这个项目不是我的,是从一位大佬那里拿来的,我将其重新实现做一个讲解,这是他的博客链接,大家可以去看看
上代码
对于这个项目,我觉得对于接口的深入理解非常好的一个项目,我这里给大家做一个详细的分析
work()方法的运行结果
首先是 Animal 动物这个总的抽象类,拥有三个方法,一个是获取名称,第二个是移动到具体的地点,第三则是喝水
然后是它的两个继承抽象类,哺乳类动物 Mammal 和爬行类动物 Reptile,因为他们均为抽象类,上面实现接口的注意事项时,有提到如果一个抽象类去继承另一个抽象类,那么它不需要重写其中的抽象方法
接着就是哺乳类动物的三个继承类老虎类、山羊类以及兔子类,爬行类动物的一个继承类蛇类,然后分别通过重写它们的祖先类中的抽象方法实现自己独有的功能
最后是一个农夫类,因为农夫可以将水带到农场给这些动物喝,这些动物只需要移动到农场即可
通过看 Farmer 类【伐木累】中的 work()方法,通过将各种动物所声明的对象传入 FeedWater()方法,从而实现了一个多态的效果,只需要通过父类引用去接收子类对象就可以实现子类具体的功能
FeedAnimal 方法的运行结果
思路分析
==首先是思路方面的分析==。上面只是引入,就下来就真正涉及到接口了,农夫除了可以将水带到农场之外,还可以将一些猎物带到农场供那些会狩猎的动物进食,那就需要就【狩猎】这个功能,但是对于狩猎,并不是每个动物都具有的,比如说山羊、兔子就不具有,因此不可以将此方法定义在抽象类 Animal 或 Mammal 中,否则会造成内存浪费
那该怎么办?因为狩猎这个行为,老虎和蛇是不同的,那难道就在它们各自的类中新增这么一个方法体吗,这就要分析了,因为不仅仅是老虎、蛇,后面这个农场可能还会新增其他的动物,比如说豹子、鳄鱼、狮子这些,它们都会捕猎,难道之后新增动物类也要将这些方法重新写在它们里面吗,这只会造成程序的内存越来越大,就会产生一系列的栈溢出、内存溢出之类的问题
这个时候就要使用到我们的接口了,首先这个功能是很多类都会使用到的,而且这个功能并不是当前这个类族的每个类成员都会使用到的,所以仅仅是为了有这个功能,为了实现一个多态性,将其定义为一个接口
代码分析
==其次是代码层面的分析==,为了方便观看,将关键代码继续粘入此处:point_down:
主要是对焦在 Farmer 农夫这个类,对于 BringAnimal()这个方法,需要的参数是农夫需要携带的猎物,以及将猎物带往的目的地
然后是 FeedAnimal()这个方法,可以看到这个是有两种方法,第一种被我注释掉了,传入的是狩猎的动物以及猎物,然后为什么要将其强转成接口类型呢,因为捕猎这个方法是在 Tiger()和 Snake()里的,而原始的抽象类 Animal 类并没有,因此要将其转为接口类型,然后通过接口回调的方式去实现多态,但是这里有一点漏洞,万一传入的这个捕猎者是其他没有捕猎能力的动物,那就会出问题了
所以第二种方案,我就将此狩猎者的对象直接给到接口,直接用接口去实现一个回调,只是在狩猎者在移动的时候需要转换成 Animal 类罢了,这样就增加了安全性,外界就无法传入没有实现 Huntable 接口的实现类到这个 FeedAnimal 中了
最后相信大家对接口中的这个 <>有所异或吧,这个是 Java 里的泛型接口,这样 hunt(T o)便可以接收多种参数,因为不一定是动物可以捕猎,人也可以捕猎,有些植物像捕蝇草也是可以捕猎的,只是所捕的猎物不同罢了,所以当你将一个接口定义成泛型接口时,要在所实现这个接口的地方加上接口的泛型标志,也就是这里 Tiger 和 Snake 类的写法
OK,说完这三个实战案例,相信你对 Java 中的接口如何使用变得更加清楚,知道了接口的真正作用,了解到定义接口的真正含义
🌳总结与提炼
看到这里【接口】,你一定是学会了,让我们来做一个总结。
首先我们先了解了什么是接口以及如何是定义并实现一个接口,然后去初步了解了接口的特点及作用。
有了这些基本知识后,就需要的知识进行一个串联,明白 UML 类图、继承类图和 UML 类图之间的细小差别。而且还要和前面的多态进行一个联系,明白上转型对象和回调函数之间的相似和差异性
当知识有了一个串联之后,就要更加进一步去了解接口中的门道,首先我们是知道了用 Lambda 表达式简化匿名内部类这个操作,提高了开发效率。接着我们便开始深入理解接口,对小伙伴们在日常中对接口可能会碰到的问题做了一个罗列,当前肯定还要其他的,可以在评论区继续提出来,我会解答的
到了最后,学完了所有知识,就要上战场了,我分别是浅到深设置了三个实战项目,更好地帮助大家进一步地理解接口、真正灵活地去使用 Java 中的接口
以上就是本文所有展现的所有内容,感谢您对本文的观看,如果疑问请于评论区留言或私信指出,谢谢:tulip:
以下是我开创的社区,感兴趣的小伙伴们可以加入进来一起交流学习,解决编程的难题
我的社区::fire:烈火神盾:fire:
版权声明: 本文为 InfoQ 作者【Fire_Shield】的原创文章。
原文链接:【http://xie.infoq.cn/article/f1925a36ab9799b338685fe46】。文章转载请联系作者。
评论