在面对对象编程中,抽象类和接口会被经常用到的俩个概念,是编程实现的基础。基于接口,可以实现对象的抽象、对象的多态、以及基于接口而非实现的设计原则等等;基于抽象类,可以实现对象的继承特性、模板设计模式等等。不过,并非所有面对对象语言都支持这俩语法概念,e.g. C++只支持抽象类,不支持接口;Python 不支持抽象类,也不支持接口。
虽然抽象类和接口经常被用到,也被经常提及,但是遇到选型时却不知所措。e.g. 接口和抽象类的区别是什么? 啥时候应该用接口?啥时候应该用抽象类?接口解决了什么编程问题? and so on ...
如果你对上面的问题不知道怎么回答,你可以往下看,否则,你可以直接返回主页看其他大大的文章了。
什么接口?什么是抽象类?俩者区别又是什么呢?
什么是接口呢? 我们先通过一个例子来了解一下接口。
public interface EventListener {
/**
* 监听事件
*/
void onEvent(Event e);
}
public class LoginEventListener implements EventListener {
@Override
public void onEvent(Event e) {
// 只处理登录事件
if(!(e instanceOf LoginEvent)) {
return;
}
// ...
}
}
public class LogoutEventListener implements EventListener {
@Override
public void onEvent(Event e) {
// 只处理登出事件
if(!(e instanceOf LogoutEvent)) {
return;
}
// ...
}
}
上面的例子是比较典型的使用场景,定义了事件监听器接口,登录事件监听器、登出事件监听器分别对登录和登出事件分别监听。
从上面的例子,我们可以看到接口有以下特点:
- 接口不能定义字段;(java 从1.8开始,支持)
- 接口不能定义方法实现,只能声明方法(java 从1.8开始,支持 default 方法,可定义函数体);
- 非抽象类实现接口时,必须实现接口声明的所有方法;
接下来,我们再看看抽象类。我们改一下上面的事例代码:
public interface EventListener {
/**
* 监听事件
*/
void onEvent(Event e);
}
public abstract class AbstractConditionalEventListener implements EventListener {
private Logger logger = LogFactory.getLogger(AbstractConditionalEventListener.class);
@Override
public final void onEvent(Event e) {
logger.debug("....");
if(match(e)) {
resolveEvent(e);
}
}
/**
* 处理事件
*/
protected abstract void resolveEvent(Event e);
/**
* 匹配事件是否需要处理
*/
protected abstract boolean match(Event e);
}
public class LoginEventListener extends AbstractConditionalEventListener {
@Override
public void resolveEvent(Event e) {
// ...
}
@Override
public boolean match(Event e) {
return e instanceof LoginEvent;
}
}
public class LogoutEventListener extends AbstractConditionalEventListener {
@Override
public void resolveEvent(Event e) {
// ...
}
@Override
public boolean match(Event e) {
return e instanceof LogoutEvent;
}
}
上面的例子,是对前面的例子做更上一层的丰富,AbstractConditionalEventListener 实现了条件化匹配事件的事件监听器,拆分了处理事件的逻辑到方法 resolveEvent,匹配事件到方法 match。LoginEventListener、LogoutEventListener 各自覆写了处理事件方法 resolveEvent 、匹配事件方法 match。
我们可以看到抽象类有以下特点:
- 抽象类不允许实例化,只能被继承;
- 抽象类可以定义属性和实现方法体;
- 抽象类被子类继承,若子类非抽象类,则必须实现所有抽象方法和接口方法;
聊了那么多,那到底接口和抽象类有什么区别呢?
语法上的区别,可以通过上面的特点,XDM都可以看到区别,这里就不说太多了。
除此之外,接口和抽象类还存在语义上的区别。
继承,是一种 is-a
的关系。比方说,猫是哺乳动物,为什么呢?因为猫具备了哺乳动物的特征,因此猫继承了哺乳动物,并在哺乳动物的基础上,衍生了属于猫的特征,比如会爬树。
而接口,是一种 has-a
的关系,意在具有某些能力,也可以认为是一种协议。比方说,电脑主板有很多接口,CPU插座接上CPU后为电脑提供运算能力,存储器插口接上内存条后为电脑提供运行内存能力等等。电脑主板上各式各样的接口,表明它有各式各样的功能,至于能力的强弱,在于怎么实现(如内存使用2G还是32G)。
抽象类解决了什么编程的痛点呢?
继承是面对对象编程的特点之一,解决了 代码复用
的问题,在这里,抽象类也同样是。多个子类可以继承抽象类中定义的属性和方法,避免重复的代码。不过,这里并不要求父类为抽象类,同样可以实现。
除了代码复用外,抽象类与继承协作,还能实现 多态
。多态提高了代码的复用性和扩展性,多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。关于多态可以看看这篇文章总结。
接口解决了什么编程的痛点呢?
抽象类更多的是为了代码复用,而接口就更侧重于解耦。接口是对行为的一种抽象,相当于一组协议或者契约,你可以联想类比一下 API 接口。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性。
实际上,接口是一个比抽象类应用更加广泛、更加重要的知识点。比如,我们经常提到的“基于接口而非实现编程”,就是一条几乎天天会用到,并且能极大地提高代码的灵活性、扩展性的设计思想。
抽象类和接口,我们应该怎么选择呢?
判断的标准非常简单。如果是 is-a
关系并且解决代码复用问题,应该选择抽象类。如果表示的是 has-a
的关系并且解决抽象问题,我们可以选择接口。
从类的继承层次上来看,抽象类是一种自下而上的设计思路
,先有子类的代码重复,然后再抽象成上层的父类(也就是抽象类)。而接口正好相反,它是一种自上而下的设计思路
。我们在编程的时候,一般都是先设计接口,再去考虑具体的实现。