在面向?qū)ο缶幊讨校橄箢惡徒涌谑莾蓚€經(jīng)常被用到的語法概念戴尸,是面向?qū)ο笏拇筇匦运诤福约昂芏嘣O(shè)計模式、設(shè)計思想孙蒙、設(shè)計原則編程實現(xiàn)的基礎(chǔ)项棠。比如,我們可以使用接口來實現(xiàn)面向?qū)ο蟮某橄筇匦钥媛汀⒍鄳B(tài)特性和基于接口而非實現(xiàn)的設(shè)計原則香追,使用抽象類來實現(xiàn)面向?qū)ο蟮睦^承特性和模板設(shè)計模式等等。
什么是抽象類和接口坦胶?區(qū)別在哪里透典?
首先,我們來看一下顿苇,在 Java 這種編程語言中峭咒,我們是如何定義抽象類的。
下面這段代碼是一個比較典型的抽象類的使用場景(模板設(shè)計模式)纪岁。Logger 是一個記錄日志的抽象類凑队,F(xiàn)ileLogger 和 MessageQueueLogger 繼承 Logger,分別實現(xiàn)兩種不同的日志記錄方式:記錄日志到文件中和記錄日志到消息隊列中幔翰。FileLogger 和 MessageQueueLogger 兩個子類復用了父類 Logger 中的 name漩氨、enabled西壮、minPermittedLevel 屬性和 log() 方法,但因為這兩個子類寫日志的方式不同叫惊,它們又各自重寫了父類中的 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(...);
}
}
通過上面的這個例子,我們來看一下赋访,抽象類具有哪些特性可都。我總結(jié)了下面三點:
抽象類不允許被實例化,只能被繼承蚓耽。也就是說渠牲,你不能 new 一個抽象類的對象出來(Logger logger = new Logger(…); 會報編譯錯誤)。
抽象類可以包含屬性和方法步悠。方法既可以包含代碼實現(xiàn)(比如 Logger 中的 log() 方法)签杈,也可以不包含代碼實現(xiàn)(比如 Logger 中的 doLog() 方法)。不包含代碼實現(xiàn)的方法叫作抽象方法鼎兽。
子類繼承抽象類答姥,必須實現(xiàn)抽象類中的所有抽象方法。對應(yīng)到例子代碼中就是谚咬,所有繼承 Logger 抽象類的子類鹦付,都必須重寫 doLog() 方法
現(xiàn)在我們再來看一下,在 Java 這種編程語言中择卦,我們?nèi)绾味x接口敲长。
// 接口
public interface Filter {
void doFilter(RpcRequest req) throws RpcException;
}
// 接口實現(xiàn)類:鑒權(quán)過濾器
public class AuthencationFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...鑒權(quán)邏輯..
}
}
// 接口實現(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é)果...
}
// ...省略其他處理邏輯...
}
}
上面這段代碼是一個比較典型的接口的使用場景。我們通過 Java 中的 interface 關(guān)鍵字定義了一個 Filter 接口秉继。AuthencationFilter 和 RateLimitFilter 是接口的兩個實現(xiàn)類祈噪,分別實現(xiàn)了對 RPC 請求鑒權(quán)和限流的過濾功能。
代碼非常簡潔尚辑。結(jié)合代碼辑鲤,我們再來看一下,接口都有哪些特性杠茬≡氯欤總結(jié)了三點:
接口不能包含屬性(也就是成員變量)。
接口只能聲明方法瓢喉,方法不能包含代碼實現(xiàn)吓坚。
類實現(xiàn)接口的時候,必須實現(xiàn)接口中聲明的所有方法灯荧。
抽象類和接口能解決什么編程問題?
首先盐杂,我們來看一下逗载,我們?yōu)槭裁葱枰橄箢惗吡克軌蚪鉀Q什么編程問題?
抽象類不能實例化厉斟,只能被繼承挚躯。而前面的章節(jié)中,我們還講到擦秽,繼承能解決代碼復用的問題码荔。所以,抽象類也是為代碼復用而生的感挥。多個子類可以繼承抽象類中定義的屬性和方法缩搅,避免在子類中,重復編寫相同的代碼触幼。
我們再來看一下硼瓣,我們?yōu)槭裁葱枰涌冢克軌蚪鉀Q什么編程問題置谦?
抽象類更多的是為了代碼復用堂鲤,而接口就更側(cè)重于解耦。接口是對行為的一種抽象媒峡,相當于一組協(xié)議或者契約瘟栖,你可以聯(lián)想類比一下 API 接口。調(diào)用者只需要關(guān)注抽象的接口谅阿,不需要了解具體的實現(xiàn)半哟,具體的實現(xiàn)代碼對調(diào)用者透明。接口實現(xiàn)了約定和實現(xiàn)相分離奔穿,可以降低代碼間的耦合性镜沽,提高代碼的可擴展性。
如何決定該用抽象類還是接口贱田?
實際上缅茉,判斷的標準很簡單。如果我們要表示一種 is-a 的關(guān)系男摧,并且是為了解決代碼復用的問題蔬墩,我們就用抽象類;如果我們要表示一種 has-a 關(guān)系耗拓,并且是為了解決抽象而非代碼復用的問題拇颅,那我們就可以使用接口。
重點:
- 抽象類和接口的語法特性抽象類不允許被實例化乔询,只能被繼承樟插。它可以包含屬性和方法。方法既可以包含代碼實現(xiàn),也可以不包含代碼實現(xiàn)黄锤。不包含代碼實現(xiàn)的方法叫作抽象方法搪缨。子類繼承抽象類,必須實現(xiàn)抽象類中的所有抽象方法鸵熟。接口不能包含屬性副编,只能聲明方法,方法不能包含代碼實現(xiàn)流强。類實現(xiàn)接口的時候痹届,必須實現(xiàn)接口中聲明的所有方法。
- 抽象類和接口存在的意義抽象類是對成員變量和方法的抽象打月,是一種 is-a 關(guān)系队腐,是為了解決代碼復用問題。接口僅僅是對方法的抽象僵控,是一種 has-a 關(guān)系香到,表示具有某一組行為特性,是為了解決解耦問題报破,隔離接口和具體的實現(xiàn)悠就,提高代碼的擴展性。
- 抽象類和接口的應(yīng)用場景區(qū)別什么時候該用抽象類充易?什么時候該用接口梗脾?實際上,判斷的標準很簡單盹靴。如果要表示一種 is-a 的關(guān)系炸茧,并且是為了解決代碼復用問題,我們就用抽象類稿静;如果要表示一種 has-a 關(guān)系梭冠,并且是為了解決抽象而非代碼復用問題,那我們就用接口改备。