抽象类:小样儿(接口),我一眼看出你就不是人(抽象类)
养成习惯,先赞后看!!!
前言
这个问题其实UP在大学期间上关于JAVA的课的时候就曾经遇到过,但是当时自己当时还是太菜了,对计算机也没什么兴趣,只是应付性的在网上查查答案,然后死记硬背,最后熬过考试.想想当初那门课程还拿了一个A,哈哈哈哈.
不扯皮了,主要是这几天UP自己重新复习JAVA基础的时候,又重新看到这个概念了,只怪当初自己没怎么了解,所以根本就没什么印象,看到之后还是根本分不清这两者到底有啥区别.所以就重新查阅了资料并且加上自己的理解.希望能够对你有帮助.
如果你觉得文章写得还可以或者文章对你有帮助的话,还请给UP赏个一键三连.UP在这里谢谢各位了.
并且由于UP自己最近在复习java基础所以会不定期的发一些自己关于基础的理解的文章,其实也有点类似于面经的的意思了啊.所以小伙伴们如果不想错过后续的文章的话,可以点击上方蓝字,关注UP.
抽象类与接口的概念
我们先介绍一下什么是抽象类什么是接口.
抽象类
:
我们看到抽象类这个词语的时候,可能本能的反应就是先想到抽象的反义词具体.其实在现实生活中存在着很多具体与抽象的例子.就比如狗,狗
是一个很抽象的概念,因为存在着很多具体的种类,像拉布拉多,金毛,边牧,哈士奇
等等,所以我们为了统一这种概念于是就将这些具体的种类全部都抽象成了一个概念即狗.就如下图所示:
抽象类其实和我们上面的例子想要表达的内容是一样的.因为我们在开发的过程一定会遇到很多这样的情况,很多的类在本质上都是存在着一定的共性
的,所以一般会把这部分的共性抽取出来作为抽象类.然后其他的具体类只需要继承该抽象类
就可以在直接实现抽象类中的方法.不需要再重复性的创建函数并实现
了.就比方我们下图所示:
到这里我们就基本上了解抽象类的基本概念了.接下来我们来了解一下什么是接口.
接口
:
其实大家在平常的开发过程中可能最经常碰到的就是接口.这个接口可能是我们自己写的接口,又或者是第三方的接口.其实本质上都是调用已经写好的方法或者是函数,从而方便我们的开发.
所以接口其实泛指的就是供给我们调用的函数或者是方法
.如果用更加直白的话来说的话:接口就是一个包含多个函数的集合
.
我们上面说了接口一般都是提供给别人调用的,所以接口基本上就是用来帮助别人扩展自己的系统或程序
的.举一个我们生活中的例子:
一般我们的灯只有开关的功能,但是呢我们现在有一个需求,就是看看能不能顺便让我们的灯监测我们的温度,如果出现温度异常的时候,就让我们的灯开始发出警告信息.这时候我们的一般做法就是在我们的灯里面集成我们的光感传感器以及报警传感器,这样我们的灯就能够实现检测温度并且异常情况下报警的功能了.如下图所示:
在这个例子里面,我们的接口就类似于上图中的两个传感器,正是因为加入了这两个传感器,我们才能够对我们一般的灯扩展监测,报警的功能.
看完现实生活中的例子之后,我们再把这个例子类比到我们的程序中应该就是下面这样的.我们定义一个类叫做Light(灯)
:
public class Light {
public void on() {
System.out.println("开灯");
}
public void off() {
System.out.println("关灯");
}
}
之后我们再定义一个叫做MonitoringAlarm(监测报警)
的接口:
public interface MonitoringAlarm {
void monitor();
void alarm();
}
之后我们就需要让我们的Light去实现MonitoringAlarm接口中的方法即可:
public class Light implements MonitoringAlarm{
public void on() {
System.out.println("开灯");
}
public void off() {
System.out.println("关灯");
}
@Override
public void monitor() {
// TODO Auto-generated method stub
System.out.println("监测温度");
}
@Override
public void alarm() {
// TODO Auto-generated method stub
System.out.println("报警");
}
}
这样我们就完成了对于灯的功能的扩展.
理解了上面例子之后,大家对于接口的基本概念就了解的差不多了.接下来我们就分析一下抽象类以及接口这两者的特点.
抽象类与接口的特点
抽象类的特点:
在程序中我们一般都是通过abstract
这个关键字来表示抽象的概念,如果某个方法或者某个类的前面加上了abstract
这个关键字,那么就代表这是一个抽象的方法或者是抽象的类.这就是我们的抽象类与一般的类的第一个区别,就比方下面我举的这两个例子:
//抽象类
public abstract class animal{}
//正常的类
public class animal{}
//抽象方法
public abstract void sleep();
大部分的文章都会强调一点那就是抽象类中一定会有抽象方法
,其实这个观点是不对的,只要这个类前面添加的abstract关键字,那么这个类就是抽象类
.只不过一般情况下抽象类中一般都会有几个抽象方法.就比方我们下面的两张图所演示的一样:
有抽象方法的抽象类
:
没有抽象方法的抽象类
:
所以不管有没有抽象方法,只要类的前面加上了abstract
这个关键字,那么这个类就是抽象类.
但是相反的,如果一个类里面有抽象方法那么这个类必定是抽象类
.否则是会直接报错的,就比方我下面两张图所演示的一样:
所以理清楚上面的逻辑之后,我们差不多也能够得到下面的推理顺序:
最后在给大家看几个例子:
//抽象类1=>没有抽象方法的抽象类
public abstract class animal{}
//抽象类2=>有抽象方法的抽象类
public abstract class animal{
public abstract void sleep();
}
//不是抽象类
public class animal{}
其次就是抽象类中的抽象方法都是没有函数体的,如果添加了函数体的话就会报错,就如下图所示:
所以抽象方法都是只包含函数的名称,不包含函数具体的执行方式.就比方下面的几个例子:
public abstract class animal{
//抽象方法没有函数体,没有函数的具体功能
public abstract void sleep();
//一般的有函数体的方法,函数里面有具体的功能
public void eat(){
System.out.println("俺要干饭");
}
}
上面我们说过了抽象类的本质还是一个类,那么java中我们常说的的对象实例化的概念,抽象类是否又可以呢?
这就是抽象类和我们一般的类的另外一个区别了.我们知道在java中万物即对象,所以一般的类都是可以直接通过new关键字进行实例化的,但是抽象类就不行,抽象类本身是不能实例化的,所以下图所示的这种创建方式是不允许的:
抽象类只能是在被别的类继承了之后,才能通过继承该抽象类的类
来进行实例化.就如我下面两张图所示:
可以看到虽然能够创建一个抽象类的对象
,但是这个实例化的过程本质上还是实例化的继承了抽象类的类
,并非是直接实例化的我们的抽象类
.
并且我们可以看到继承了抽象类的类是必须实现抽象类的抽象方法的,否则还是会报错的,如我下面两张图所示:
并且每个类只能继承并实现一个抽象类,不能继承多个抽象类,否则就会报错,如下图:
我们可以看到每个类只能继承并实现一个抽象类.
最后我们总结一下抽象类的特点有哪些:
- 必须通过
abstract
关键字修饰 - 抽象类中
不一定
有抽象方法,可以有也可以没有. - 抽象类既可以有抽象方法,也可以有一般方法
- 抽象类的抽象方法
不能
包含方法体 - 抽象类
不能实例化
,只能通过继承了抽象类的类来进行实例化 - 继承了抽象类的类
必须
要实现抽象类中的抽象方法,并且只能继承一个抽象类
接口的特点:
接口一般是通过interface
关键字来定义的,一个简单的接口定义就如下面的代码一样:
public interface advice {
}
上面的接口是一个最简单的接口.往往我们的接口里面会包含很多的函数,就比方下面这个例子:
并且在这个接口里面我们不仅能够看到有多个函数
,同时该接口里面还有自定义的变量
.
这里还要提醒大家一点的就是:因为这里面不管是方法还是变量都是只通过public
来进行修饰的,所以大家可能本能的就认为这些方法以及变量就是公开的,其实并不是这样的,接口里的所有方法其实都是抽象方法,接口里的所有变量也其实都是通过static final
关键字来修饰的.这里我们可以通过查看类的结构就可以看到,就如下图所示:
大家其实仔细看变量名以及方法名的上面都是有字母的,其实大家现在想想应该能知道那些字母分表代表什么意思了.
SF就代表static final
A就代表abstract
所以并不是我们所想象的那样,知道了方法都是抽象方法了之后,大家这时候结合我们上面所讲的抽象方法的概念,大家应该也能基本了解接口中方法的特点了.方法我们理解完了,那现在我们再来讲讲接口中的变量吧.大家知道的,既然接口中的变量已经通过static final
修饰了那么就以为着这个变量是无法重新进行赋值
的,相当于是一个值已经恒定的变量
了.
就如同我下面图里面所演示的一样:
这里可以给大家看一下错误的提示信息:
The final field advice.length cannot be assigned
意思就是我们上面讲的意思.所以一般在开发中是不在接口中定义变量的,如果需要在接口中定义变量并且来使用的话,那么很明显这个变量在项目中就是担当一个常量的作用.
其次我们在来看看我们在类中都是如何使用接口的:
我们都是通过implements
关键字来实现我们的接口的.从上面的图中我们也可以看到如果一个类要使用接口的,那么就和一个类要继承抽象类一样,那么都是必须要实现该接口或者是抽象类中抽象方法的
,否则也是会报错的.并且我们允许一个类可以实现多个接口,就如下图所示:
最后我们总结一下接口的特点有哪些:
- 接口必须通过
interface
关键字来修饰 - 接口中的方法都是
抽象
的,并且即使是变量也是无法更改
的. - 使用接口的类必须实现接口中的
所有方法
- 一个类可以实现
多个
接口,并且需要实现这些接口中的所有方法.
到这里我们基本上就已经了解了抽象类以及接口的基本特点了.那么接下来就是我们的重点了:他们两者之间到底什么样的关系呢?让我们接着往下看.
抽象类与接口的区别
如果上面的内容你都已经认真看过之后,不知道大家是否会有这样的疑问:抽象类和接口真的好像啊,都有抽象方法,一般的类继承抽象类或者是实现接口的时候,同样都需要实现他们两者的抽象方法.
当初我看网上的文章基本都没有说到他们的内核问题
上面,所以当时自己一直都有这样的疑惑.所以不知道大家有没有这样的疑惑.如果有的话,就请继续看我下面的内容,相信一定可以给你一个很好的解答.
首先我们的确需要承认的就是其实接口的确就是从抽象类中分离出来的,所以接口很有很多抽象类的特征.所以大家才会觉得接口与抽象类非常的相似.但是他们两者适用的的范围其实是不一样的.接口从抽象类中分离出来,肯定不是为了实现和抽象类同样的功能的.接下来我们详细的来讲解.
上面我们说过了抽象类是所有具体的个体类所共有的特征,所以我们是不是可以用下面的图来描述个体类与抽象类的关系呢,看下图:
从上面的图我们基本能得出这样的结论:抽象类与个体类是从属
的关系.而且我们之前说过个体类实现抽象类的时候说的是继承,通过继承这个关键词大家也能够理解为什么是从属关系了.看完抽象类与个体类的关系之后我们再来看看接口与我们类的关系.
上面我们讲解接口的时候曾经说过,接口时用来扩展我们的系统或者是程序的.所以接口和我们的类的关系是不是可以用下面的图来描述呢,如下图:
从上图中我们就既可以看出来接口和我们的个体类的关系是需不需要
的关系.如果一个个体类本身就应该有这个功能,那么很显然我们就需要为这个类实现相应的接口并且上面我们也说过一个类是可以实现多个接口的,正就刚好表现了需不需要的概念.只要我想,我就可以全都需要.
这就是抽象类与接口的第一个区别.为了方便大家更好的理解这个概念.我们还是举上面我们关于狗的例子.
现在我们我们现在有两个抽象类分别为Dog
:
public abstract class Dog {
public abstract void eat();
public abstract void wang();
public abstract void sleep();
}
Cat
:
public abstract class Cat {
public abstract void eat();
public abstract void miao();
public abstract void sleep();
}
假设我们需要创建一个Husky
(哈士奇)的类,那么首先我们就需要先选择我们该将这个类继承上面我们定义的那个抽象类呢?很明显Husky
是属于Dog
的而并不是属于Cat
的范畴.所以这一步不仅解释了抽象类与个体类是从属的关系,并且还解释了类只能继承一个抽象类.
接下来我们继承完相应的抽象类之后,我们首先就需要先实现抽象类中的抽象方法,代码如下:
public class Husky extends Dog{
@Override
public void eat() {
// TODO Auto-generated method stub
System.out.println("邋里邋遢哈士奇,到处拆家惹人厌");
System.out.println("吃饭?没空吃饭,拆家呢!");
}
@Override
public void sleep() {
// TODO Auto-generated method stub
System.out.println("睡觉?家还没拆完,睡什么觉!");
}
@Override
public void wang() {
// TODO Auto-generated method stub
System.out.println("呜呜呜....呜呜呜呜呜呜.....");
}
}
这些就是我们哈士奇的基本特征吗,很显然哈士奇和其他的狗有一个基本的区别就是哈士奇会:
所以很显然这是哈士奇的一个最明显的特征同时也是哈士奇一定会的一项技能,所以我们需要为哈士奇这个类扩展这个功能,所以这时候我们就创建一个叫做DestroyHome
的接口:
public interface DestroyHome {
void destroyhome();
}
这时候我们重新让Husky
实现这个接口:
public class Husky extends Dog implements DestroyHome{
@Override
public void eat() {
// TODO Auto-generated method stub
System.out.println("邋里邋遢哈士奇,到处拆家惹人厌");
System.out.println("吃饭?没空吃饭,拆家呢!");
}
@Override
public void sleep() {
// TODO Auto-generated method stub
System.out.println("睡觉?家还没拆完,睡什么觉!");
}
@Override
public void wang() {
// TODO Auto-generated method stub
System.out.println("呜呜呜....呜呜呜呜呜呜.....");
}
@Override
public void destroyhome() {
// TODO Auto-generated method stub
System.out.println("你还想有家?我给你全拆光");
}
}
到这里我们关于Husky
就基本上编写完毕了.在这个过程中大家可以更加的理解抽象类是一个属不属于
的问题,接口是一个需不需要
的问题.
其次就是在代码的编写
上面,虽然两者本质上都是抽象的,但是抽象类属于是显式抽象
,接口属于是隐式抽象
.这个很好理解.抽象类的一个硬性条件就是必须要有abstract
关键字的修饰,而接口则不需要我们将abstract
这个关键字显示的编写在我们的类以及方法名的前面.
接着就是在设计模式
的层面上,还是像我们上面说的那样,抽象类是所有具体个例都抽象出来的所有具体个例所拥有的统一的类.意思就是具体个例的一般性行为,抽象类中都会有.还是拿我们的狗
来举例:
世界上有很多种类的狗,有金毛,边牧,拉布拉多,哈士奇等,他们的行为一定会因为品种的问题存在着很大的差异
,就比如说哈士奇,他的拆家属性肯定是点满了的.但是呢这些不同种类的狗都会有狗的一些共性,就比如说吃,喝,睡觉.这些行为都是狗的一些共性,所以我们就可以直接在Dog这个抽象类里面直接定义这些方法,然后这些个例只需要继承抽象类实现这些方法即可.不用在单独在每个类里面再创建这些行为,再实现.就好比下图所示:
所以说抽象类就像是一个模板
,他是所有具体个例的母版.所有的具体个例都会模仿抽象类的行为.并且假设所有的个例的某个行为都发生了变化,那么我们就只需要通过修改模版即抽象类即可解决问题.
但是接口和抽象类却有所不同.就像我们上面说的一样,接口是用来扩展我们的系统或者程序的,说以很显然接口属于是一种个性化定制
服务,所接口一旦发生了改变的话,那么很显然只要是实现了该接口的类就全部需要进行修改.就比方下图演示的过程:
假设现在我们把接口中的play方法删掉了,改成了amuse方法,那么很显然所有只要实现了这个该接口的类都需要删掉play方法,并且实现amuse方法,入下图所示:
所以很显然接口属于是一种辐射状
的设计,属于是一点向多点的扩散.这和我们的抽象类恰恰相反.但是不知道大家有没有想过是什么原因造成了这样的状况呢?
其实这个问题是很简单的.大家还记不记得我们上面讲过的.抽象类中即可以包含抽象
方法,同时也可以包含一般
方法.相反的接口中的方法默认都是抽象
方法.正因为抽象类中可以存在一般方法,所以抽象类可以直接修改本身的方法而不需要在修改其他继承该抽象类中的方法.接口就不行了,因为全是抽象方法,所以一旦需要修改就只能修改每一个类中的方法.
最后我们总结一下抽象类与接口的区别:
- 抽象类是
显式
抽象,接口是隐式
抽象 - 抽象类中既可以有抽象方法,也可以有一般方法,但是接口中的方法默认都是抽象的
- 抽象类解决的是
"是不是"
的问题,接口解决的是"有没有"
的问题 - 一个类只能继承
一个抽象类
,但是能实现多个接口
最后的最后我们还是通过一个例子来帮助我们收官.网上经常讲的例子就是防盗门,但是我们本片文章一直再举狗的例子,所以最后一个例子我们还是来举狗的例子
好了话不多少,正式开始我们最后一个例子:
假设我们需要定义一个叫做Husky
(哈士奇)的类,这个类现在需要实现这么几个功能:吃饭
,睡觉
,喝水
,拆家
,发疯
.如何通过抽象类Dog
或者接口Behavior
来实现呢?
这里假设我们将所有的方法都定义在了抽象类Dog
中.代码如下:
public abstract class Dog {
public abstract void eat();
public abstract void sleep();
public abstract void drink();
public abstract void destoryhome();
public abstract void crazy();
}
很明显我们值需要通过让我们的Husky
继承该抽象类并且实现里面的抽象方法就行了.代码如下:
public class Husky extends Dog{
@Override
public void eat() {
// TODO Auto-generated method stub
}
@Override
public void sleep() {
// TODO Auto-generated method stub
}
@Override
public void drink() {
// TODO Auto-generated method stub
}
@Override
public void destoryhome() {
// TODO Auto-generated method stub
}
@Override
public void crazy() {
// TODO Auto-generated method stub
}
}
这样很显然也能够直接完成我们的目标,但是这里会有一个问题那就是当我们创建其他的狗的品种类时,就比如金毛,边牧的时候,很明显我们也是需要去继承Dog这个抽象类的,但是这时候我们会发现,这个抽象类中的两个方法是多余
的,因为金毛和边牧基本上不会拆家,发疯
,所以很明显这两个函数就是多余的.只要继承了这个抽象类,那么就必须要是实现这两个方法.所以虽然这种方案可以完成我们的任务,但是很明显不利于我们之后项目的扩展
.
这时候有小伙伴可能会说,那我们直接在接口中定义这些方法不就行了.大家想一想就会发现,其实会出想我们上面遇到的同样的问题.
不知道大家这时候是不是已经有思路了呢.这时候结合我们上面所说的.抽象类是所有个例的共性,接口属于是个性化定制的范畴.看到这两句话,相信大家已经有了答案. 最好的方案应该是这样设计:
Dog
抽象类:
public abstract class Dog {
public abstract void eat();
public abstract void sleep();
public abstract void drink();
}
Behavior
接口:
public interface Behavior{
void destoryhome();
void crazy();
}
最后我们的Husky
只需要这样继承Dog
抽象类以及实现Behavior
接口即可,代码如下:
public class Husky extends Dog implements Behavior{
@Override
public void eat() {
// TODO Auto-generated method stub
}
@Override
public void sleep() {
// TODO Auto-generated method stub
}
@Override
public void drink() {
// TODO Auto-generated method stub
}
@Override
public void destoryhome() {
// TODO Auto-generated method stub
}
@Override
public void crazy() {
// TODO Auto-generated method stub
}
}
这样当我们创建其他类继承Dog的时候吧就不用再去是此案那两个多余的方法了.并且接口也是定制化,适用于所有包含拆家以及发疯的动物.这样就能醉倒更好的扩展我们的程序了.
到这里我们关于抽象类以及接口的讲解就已经全部讲解完毕了,如果觉得文章不错或者觉得UP写的还可以的话,可以关注我的公众号:萌萌哒的瓤瓤
.
原创不易,码字不易!!!新人UP需要你的支持!!!