抽象类和接口的区别
从语法层次上
补充:
抽象类实现接口时,可以将接口中的方法放在非抽象子类中实现,也可以就在抽象类中实现。(其实就是在实现一个接口中的抽象方法,考虑抽象类中抽象方法的实现方式)。
从设计层次上
- 抽象层次不同。抽象类是对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
- 跨域不同。抽象类跨域的是具有相似特点的类,而接口却可以跨域不同的类。抽象类是从子类中发现公共部分,然后泛化成抽象类,子类继承该父类即可;但是接口不同,实现它的子类可以不存在任何关系,共同之处。例如猫、狗可以抽象成一个动物类抽象类,具备叫的方法。鸟、飞机可以实现飞(Fly)接口,具备飞的行为,但我们不能将鸟、飞机共用一个父类!所以说抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is-a" 关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的, 仅仅是实现了接口定义的契约而已。
- 设计层次不同。对于抽象类,它是自下而上来设计的,我们要先知道多个子类的共同点,才能抽象出父类;而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于实现它的子类是什么,子类什么时候、怎么实现它一概不知。比如我们只有一个猫类,如果你把它抽象成一个动物类,未免设计过度;我们起码要有两个动物类,猫、狗,之后抽象出他们的共同点形成动物抽象类!所以说抽象类往往都是通过重构而来的!但是接口则不同,比如说 飞,我们根本就不知道会有什么东西来实现这个飞 接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
实例
例一
假设我们有一个Door的抽象概念,它具备两个行为open()和close(),此时我们可以通过抽象类或接口来定义这个抽象概念。
如果使用抽象类:
abstract class Door{
public abstract void open();
public abstract void close();
}
如果使用接口:
interface Door{
public abstract void open();
public abstract void close();
}
之后可以使用extends继承抽象类Door或者使用implements实现接口Door,来实现具体类。此时两者并没有什么很大的差异。
但是现在如果我们需要门具有报警的功能,那么该如何实现呢?
解决方案一:给Door增加一个报警方法:clarm();
abstract class Door{
public abstract void open();
public abstract void close();
public abstract void alarm();
}
或者:
interface Door{
public abstract void open();
public abstract void close();
public abstract void alarm();
}
但是,这种方法违反了面向对象设计中的一个核心原则 ISP,在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起(门一定可以开、关,但不一定可以报警)。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变而改变,反之依然。
解决方案二:
既然open()、close()和alarm()属于两个不同的概念,那么我们依据ISP原则将它们分开定义在两个代表不同概念的抽象类里面,定义的方式有三种:
- 两个概念都使用抽象类来定义。
- 两个概念都使用接口来定义。
- 一个概念使用抽象类定义,一个概念使用接口定义。
由于java不支持多继承,所以第一种是不可行的。(一个子类不能既继承一个抽象类的开、关方法,又继承另一个抽象类的alarm方法)。后面两种都是可行的,但是选择何种就反映了你对问题域本质的理解。
如果选择第二种方式,那么从两个接口的设计方式中反映出了两个问题:
- 我们可能没有理解清楚问题域,AlarmDoor在概念本质上到底是门还是报警器。因为分不清,所以写了两个接口,让它既是门也是报警器。或者:
- 如果我们对问题域的理解没有问题,比如我们在分析时确定了AlarmDoor和Door在本质上概念是一致的,那么我们在设计时就没有正确的反映出我们的设计意图。因为你使用了两个接口来进行定义,他们概念的定义并不能够反映上述含义。
第三种,如果我们对问题域的理解是这样的:AlarmDoor本质上是Door,但同时它也拥有报警的行为功能,这个时候我们使用第三种方案恰好可以阐述我们的设计意图。AlarmDoor本质上是门,所以对于这个概念我们使用抽象类来定义,同时AlarmDoor还具备报警功能,说明它能够完成报警概念中定义的行为功能,所以alarm可以使用接口来进行定义。代码如下:
abstract class Door{
public abstract void open();
public abstract void close();
}
interface Alarm{
public abstract void alarm();
}
class AlarmDoor extends Door implements Alarm{
public void open(){ }
public void close(){ }
public void alarm(){ }
}
这种实现方式基本上能够明确的**反映出我们对于问题领域的理解,正确的揭示我们的设计意图**。其实抽象类表示的是"is-a"关系,接口表示的是"like-a"关系,在选择时可以作为一个依据,但这必须建立在对问题领域的理解上。比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。
一些参考原则
- 在进行某些公共操作的时候一定要定义出接口。
- 如果是我们自己写的接口,尽量不要使用关键字new去直接实例化接口子类,要使用工厂类完成。
- 有了接口就需要利用子类完善方法。