理論五:接口vs抽象類的區(qū)別?如何用普通的類模擬抽象類和接口?

在面向?qū)ο缶幊讨?,抽象類和接口是兩個(gè)經(jīng)常被用到的語法概念,是面向?qū)ο笏拇筇匦?,以及很多設(shè)計(jì)模式、設(shè)計(jì)思想、設(shè)計(jì)原則編程實(shí)現(xiàn)的基礎(chǔ)。比如,我們可以使用接口來實(shí)現(xiàn)面向?qū)ο蟮某橄筇匦?、多態(tài)特性和基于接口而非實(shí)現(xiàn)的設(shè)計(jì)原則,使用抽象類來實(shí)現(xiàn)面向?qū)ο蟮睦^承特性和模板設(shè)計(jì)模式等等。

什么是抽象類和接口?區(qū)別在哪里?
首先,我們來看一下,在 Java 這種編程語言中,我們是如何定義抽象類的。
下面這段代碼是一個(gè)比較典型的抽象類的使用場景(模板設(shè)計(jì)模式)。Logger 是一個(gè)記錄日志的抽象類,F(xiàn)ileLogger 和 MessageQueueLogger 繼承 Logger,分別實(shí)現(xiàn)兩種不同的日志記錄方式:記錄日志到文件中和記錄日志到消息隊(duì)列中。FileLogger 和 MessageQueueLogger 兩個(gè)子類復(fù)用了父類 Logger 中的 name、enabled、minPermittedLevel 屬性和 log() 方法,但因?yàn)檫@兩個(gè)子類寫日志的方式不同,它們又各自重寫了父類中的 doLog() 方法。

// 抽象類
public abstract class Logger {
private String name;
private boolean enabled;
private Level minPermittedLevel;

public Logger(String name, boolean enabled, Level minPermittedLevel) {
this.name = name;
this.enabled = enabled;
this.minPermittedLevel = minPermittedLevel;
}

public void log(Level level, String message) {
boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
if (!loggable) return;
doLog(level, message);
}

protected abstract void doLog(Level level, String message);
}
// 抽象類的子類:輸出日志到文件
public class FileLogger extends Logger {
private Writer fileWriter;

public FileLogger(String name, boolean enabled,
Level minPermittedLevel, String filepath) {
super(name, enabled, minPermittedLevel);
this.fileWriter = new FileWriter(filepath);
}

@Override
public void doLog(Level level, String mesage) {
// 格式化level和message,輸出到日志文件
fileWriter.write(...);
}
}
// 抽象類的子類: 輸出日志到消息中間件(比如kafka)
public class MessageQueueLogger extends Logger {
private MessageQueueClient msgQueueClient;

public MessageQueueLogger(String name, boolean enabled,
Level minPermittedLevel, MessageQueueClient msgQueueClient) {
super(name, enabled, minPermittedLevel);
this.msgQueueClient = msgQueueClient;
}

@Override
protected void doLog(Level level, String mesage) {
// 格式化level和message,輸出到消息中間件
msgQueueClient.send(...);
}
}

通過上面的這個(gè)例子,我們來看一下,抽象類具有哪些特性。我總結(jié)了下面三點(diǎn):
抽象類不允許被實(shí)例化,只能被繼承。也就是說,你不能 new 一個(gè)抽象類的對(duì)象出來(Logger logger = new Logger(…); 會(huì)報(bào)編譯錯(cuò)誤)。
抽象類可以包含屬性和方法。方法既可以包含代碼實(shí)現(xiàn)(比如 Logger 中的 log() 方法),也可以不包含代碼實(shí)現(xiàn)(比如 Logger 中的 doLog() 方法)。不包含代碼實(shí)現(xiàn)的方法叫作抽象方法。
子類繼承抽象類,必須實(shí)現(xiàn)抽象類中的所有抽象方法。對(duì)應(yīng)到例子代碼中就是,所有繼承 Logger 抽象類的子類,都必須重寫 doLog() 方法

現(xiàn)在我們再來看一下,在 Java 這種編程語言中,我們?nèi)绾味x接口。

// 接口
public interface Filter {
void doFilter(RpcRequest req) throws RpcException;
}
// 接口實(shí)現(xiàn)類:鑒權(quán)過濾器
public class AuthencationFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...鑒權(quán)邏輯..
}
}
// 接口實(shí)現(xiàn)類:限流過濾器
public class RateLimitFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...限流邏輯...
}
}
// 過濾器使用demo
public class Application {
// filters.add(new AuthencationFilter());
// filters.add(new RateLimitFilter());
private List<Filter> filters = new ArrayList<>();

public void handleRpcRequest(RpcRequest req) {
try {
for (Filter filter : fitlers) {
filter.doFilter(req);
}
} catch(RpcException e) {
// ...處理過濾結(jié)果...
}
// ...省略其他處理邏輯...
}
}

上面這段代碼是一個(gè)比較典型的接口的使用場景。我們通過 Java 中的 interface 關(guān)鍵字定義了一個(gè) Filter 接口。AuthencationFilter 和 RateLimitFilter 是接口的兩個(gè)實(shí)現(xiàn)類,分別實(shí)現(xiàn)了對(duì) RPC 請(qǐng)求鑒權(quán)和限流的過濾功能。
代碼非常簡潔。結(jié)合代碼,我們再來看一下,接口都有哪些特性??偨Y(jié)了三點(diǎn):
接口不能包含屬性(也就是成員變量)。
接口只能聲明方法,方法不能包含代碼實(shí)現(xiàn)。
類實(shí)現(xiàn)接口的時(shí)候,必須實(shí)現(xiàn)接口中聲明的所有方法。

抽象類和接口能解決什么編程問題?
首先,我們來看一下,我們?yōu)槭裁葱枰橄箢??它能夠解決什么編程問題?
抽象類不能實(shí)例化,只能被繼承。而前面的章節(jié)中,我們還講到,繼承能解決代碼復(fù)用的問題。所以,抽象類也是為代碼復(fù)用而生的。多個(gè)子類可以繼承抽象類中定義的屬性和方法,避免在子類中,重復(fù)編寫相同的代碼。

我們再來看一下,我們?yōu)槭裁葱枰涌冢克軌蚪鉀Q什么編程問題?
抽象類更多的是為了代碼復(fù)用,而接口就更側(cè)重于解耦。接口是對(duì)行為的一種抽象,相當(dāng)于一組協(xié)議或者契約,你可以聯(lián)想類比一下 API 接口。調(diào)用者只需要關(guān)注抽象的接口,不需要了解具體的實(shí)現(xiàn),具體的實(shí)現(xiàn)代碼對(duì)調(diào)用者透明。接口實(shí)現(xiàn)了約定和實(shí)現(xiàn)相分離,可以降低代碼間的耦合性,提高代碼的可擴(kuò)展性。

如何決定該用抽象類還是接口?
實(shí)際上,判斷的標(biāo)準(zhǔn)很簡單。如果我們要表示一種 is-a 的關(guān)系,并且是為了解決代碼復(fù)用的問題,我們就用抽象類;如果我們要表示一種 has-a 關(guān)系,并且是為了解決抽象而非代碼復(fù)用的問題,那我們就可以使用接口。

重點(diǎn):

  1. 抽象類和接口的語法特性抽象類不允許被實(shí)例化,只能被繼承。它可以包含屬性和方法。方法既可以包含代碼實(shí)現(xiàn),也可以不包含代碼實(shí)現(xiàn)。不包含代碼實(shí)現(xiàn)的方法叫作抽象方法。子類繼承抽象類,必須實(shí)現(xiàn)抽象類中的所有抽象方法。接口不能包含屬性,只能聲明方法,方法不能包含代碼實(shí)現(xiàn)。類實(shí)現(xiàn)接口的時(shí)候,必須實(shí)現(xiàn)接口中聲明的所有方法。
  2. 抽象類和接口存在的意義抽象類是對(duì)成員變量和方法的抽象,是一種 is-a 關(guān)系,是為了解決代碼復(fù)用問題。接口僅僅是對(duì)方法的抽象,是一種 has-a 關(guān)系,表示具有某一組行為特性,是為了解決解耦問題,隔離接口和具體的實(shí)現(xiàn),提高代碼的擴(kuò)展性。
  3. 抽象類和接口的應(yīng)用場景區(qū)別什么時(shí)候該用抽象類?什么時(shí)候該用接口?實(shí)際上,判斷的標(biāo)準(zhǔn)很簡單。如果要表示一種 is-a 的關(guān)系,并且是為了解決代碼復(fù)用問題,我們就用抽象類;如果要表示一種 has-a 關(guān)系,并且是為了解決抽象而非代碼復(fù)用問題,那我們就用接口。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容