什么是抽象類(lèi)和接口? 區(qū)別在哪里?
不同的編程語(yǔ)言對(duì)接口和抽象類(lèi)的定義方式可能有些差別,但是差別并不大。本文使用 Java 語(yǔ)言。
抽象類(lèi)
下面我們通過(guò)一個(gè)例子來(lái)看一個(gè)典型的抽象類(lèi)的使用場(chǎng)景。
Logger 是一個(gè)記錄日志的抽象類(lèi),F(xiàn)ileLogger 和 MessageQueueLogger 繼承Logger,分別實(shí)現(xiàn)兩種不同的日志記錄方式:
- 記錄日志到文件中
- 記錄日志到消息隊(duì)列中
FileLogger 和 MessageQueuLogger 兩個(gè)子類(lèi)復(fù)用了父類(lèi) Logger 中的name、enabled 以及 minPermittedLevel 屬性和 log 方法,但是因?yàn)閮蓚€(gè)子類(lèi)寫(xiě)日志的方式不同,他們又各自重寫(xiě)了父類(lèi)中的doLog方法。
父類(lèi)
import java.util.logging.Level;
/**
* 抽象父類(lèi)
* @author yanliang
* @date 9/27/2020 5:59 PM
*/
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);
}
FileLogger
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.logging.Level;
/**
* 抽象類(lèi)Logger的子類(lèi):輸出日志到文件中
* @author yanliang
* @date 9/28/2020 4:44 PM
*/
public class FileLogger extends Logger {
private Writer fileWriter;
public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filePath) throws IOException {
super(name, enabled, minPermittedLevel);
this.fileWriter = new FileWriter(filePath);
}
@Override
protected void doLog(Level level, String message) {
// 格式化level 和 message,輸出到日志文件
fileWriter.write(...);
}
}
MessageQueuLogger
import java.util.logging.Level;
/**
* 抽象類(lèi)Logger的子類(lèi):輸出日志到消息隊(duì)列中
* @author yanliang
* @date 9/28/2020 6:39 PM
*/
public class MessageQueueLogger extends Logger {
private MessageQueueClient messageQueueClient;
public MessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQueueClient messageQueueClient) {
super(name, enabled, minPermittedLevel);
this.messageQueueClient = messageQueueClient;
}
@Override
protected void doLog(Level level, String message) {
// 格式化level 和 message,輸出到消息隊(duì)列中
messageQueueClient.send(...)
}
}
通過(guò)上面的例子,我們來(lái)看下抽象類(lèi)有哪些特性。
- 抽象類(lèi)不能被實(shí)例化,只能被繼承。(new 一個(gè)抽象類(lèi),會(huì)報(bào)編譯錯(cuò)誤)
- 抽象類(lèi)可以包含屬性和方法。方法既可以包含實(shí)現(xiàn),也可以不包含實(shí)現(xiàn)。不包含實(shí)現(xiàn)的方法叫做抽象方法
- 子類(lèi)繼承抽象類(lèi),必須實(shí)現(xiàn)抽象類(lèi)中的所有抽象方法。
接口
同樣的,下面我們通過(guò)一個(gè)例子來(lái)看下接口的使用場(chǎng)景。
/**
* 過(guò)濾器接口
* @author yanliang
* @date 9/28/2020 6:46 PM
*/
public interface Filter {
void doFilter(RpcRequest req) throws RpcException;
}
/**
* 接口實(shí)現(xiàn)類(lèi):鑒權(quán)過(guò)濾器
* @author yanliang
* @date 9/28/2020 6:48 PM
*/
public class AuthencationFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
// 鑒權(quán)邏輯
}
}
/**
* 接口實(shí)現(xiàn)類(lèi):限流過(guò)濾器
* @author yanliang
* @date 9/28/2020 6:48 PM
*/
public class RateLimitFilter implements Filter{
@Override
public void doFilter(RpcRequest req) throws RpcException {
// 限流邏輯
}
}
/**
* 過(guò)濾器使用demo
* @author yanliang
* @date 9/28/2020 6:48 PM
*/
public class Application {
// 過(guò)濾器列表
private List<Filter> filters = new ArrayList<>();
filters.add(new AuthencationFilter());
filters.add(new RateLimitFilter());
public void handleRpcRequest(RpcRequest req) {
try {
for (Filter filter : filters) {
filter.doFilter(req);
}
} catch (RpcException e) {
// 處理過(guò)濾結(jié)果
}
// ...
}
}
上面的案例是一個(gè)典型的接口使用場(chǎng)景。通過(guò)Java中的 interface 關(guān)鍵字定義了一個(gè)Filter 接口,AuthencationFilter 和 RetaLimitFilter 是接口的兩個(gè)實(shí)現(xiàn)類(lèi),分別實(shí)現(xiàn)了對(duì)Rpc請(qǐng)求的鑒權(quán)和限流的過(guò)濾功能。
下面我們來(lái)看下接口的特性:
- 接口不能包含屬性(也就是成員變量)
- 接口只能聲明方法,方法不能包含代碼實(shí)現(xiàn)
- 類(lèi)實(shí)現(xiàn)接口時(shí),必須實(shí)現(xiàn)接口中生命的所有方法。
綜上,從語(yǔ)法上對(duì)比,這兩者有比較大的區(qū)別,比如抽象類(lèi)中可以定義屬性、方法的實(shí)現(xiàn),而接口中不能定義屬性,方法也不能包含實(shí)現(xiàn)等。
除了語(yǔ)法特性的不同外,從設(shè)計(jì)的角度,這兩者也有較大區(qū)別。抽象類(lèi)本質(zhì)上就是類(lèi),只不過(guò)是一種特殊的類(lèi),這種類(lèi)不能被實(shí)例化,只能被子類(lèi)繼承。屬于is-a的關(guān)系。接口則是 has-a 的關(guān)系,表示具有某些功能。對(duì)于接口,有一個(gè)更形象的叫法:協(xié)議(contract)
抽象類(lèi)和接口解決了什么問(wèn)題?
下面我們先來(lái)思考一個(gè)問(wèn)題~
抽象類(lèi)的存在意義是為了解決代碼復(fù)用的問(wèn)題(多個(gè)子類(lèi)可以繼承抽象類(lèi)中定義的屬性哈方法,避免在子類(lèi)中,重復(fù)編寫(xiě)相同的代碼)。
那么,既然繼承本身就能達(dá)到代碼復(fù)用的目的,而且繼承也不一定非要求是抽象類(lèi)。我們不適用抽象類(lèi),貌似也可以實(shí)現(xiàn)繼承和復(fù)用。從這個(gè)角度上講,我們好像并不需要抽象類(lèi)這種語(yǔ)法呀。那抽象類(lèi)除了解決代碼復(fù)用的問(wèn)題,還有其他存在的意義嗎?
這里大家可以先思考一下哈~
我們還是借用上面Logger的例子,首先對(duì)上面的案例實(shí)現(xiàn)做一些改造。在改造之后的實(shí)現(xiàn)中,Logger不再是抽象類(lèi),只是一個(gè)普通的父類(lèi),刪除了Logger中的兩個(gè)方法,新增了 isLoggable()方法。FileLogger 和 MessageQueueLogger 還是繼承Logger父類(lèi)已達(dá)到代碼復(fù)用的目的。具體代碼如下:
/**
* 父類(lèi):非抽象類(lèi),就是普通的類(lèi)
* @author yanliang
* @date 9/27/2020 5:59 PM
*/
public 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 boolean isLoggable(Level level) {
return enabled && (minPermittedLevel.intValue() <= level.intValue());
}
}
/**
* 抽象類(lèi)Logger的子類(lèi):輸出日志到文件中
* @author yanliang
* @date 9/28/2020 4:44 PM
*/
public class FileLogger extends Logger {
private Writer fileWriter;
public FileLogger(String name, boolean enabled, Level minPermittedLevel, String filePath) throws IOException {
super(name, enabled, minPermittedLevel);
this.fileWriter = new FileWriter(filePath);
}
protected void log(Level level, String message) {
if (!isLoggable(level)) return ;
// 格式化level 和 message,輸出到日志文件
fileWriter.write(...);
}
}
package com.yanliang.note.java.abstract_demo;
import java.util.logging.Level;
/**
* 抽象類(lèi)Logger的子類(lèi):輸出日志到消息隊(duì)列中
* @author yanliang
* @date 9/28/2020 6:39 PM
*/
public class MessageQueueLogger extends Logger {
private MessageQueueClient messageQueueClient;
public MessageQueueLogger(String name, boolean enabled, Level minPermittedLevel, MessageQueueClient messageQueueClient) {
super(name, enabled, minPermittedLevel);
this.messageQueueClient = messageQueueClient;
}
protected void log(Level level, String message) {
if (!isLoggable(level)) return ;
// 格式化level 和 message,輸出到消息隊(duì)列中
messageQueueClient.send(...)
}
}
以上實(shí)現(xiàn)雖然達(dá)到了代碼復(fù)用的目的(復(fù)用了父類(lèi)中的屬性),但是卻無(wú)法使用多態(tài)的特性了。
像下面這樣編寫(xiě)代碼就會(huì)出現(xiàn)編譯錯(cuò)誤,因?yàn)長(zhǎng)ogger中并沒(méi)有定義log()方法。
Logger logger = new FileLogger("access-log", true, Level.WARN, "/user/log");
logger.log(Level.ERROR, "This is a test log message.");
如果我們?cè)诟割?lèi)中,定義一個(gè)空的log()方法,讓子類(lèi)重寫(xiě)父類(lèi)的log()方法,實(shí)現(xiàn)自己的記錄日志邏輯。使用這種方式是否能夠解決上面的問(wèn)題呢? 大家可以先思考下~
這個(gè)思路可以用使用,但是并不優(yōu)雅,主要有一下幾點(diǎn)原因:
- 在Logger中定義一個(gè)空的方法,會(huì)影響代碼的可讀性。如果不熟悉Logger背后的設(shè)計(jì)思想,又沒(méi)有代碼注釋的話(huà),在閱讀Logger代碼時(shí)就會(huì)感到疑惑(為什么這里會(huì)存在一個(gè)空的log()方法)
- 當(dāng)創(chuàng)建一個(gè)新的子類(lèi)繼承Logger父類(lèi)時(shí),有時(shí)可能會(huì)忘記重新實(shí)現(xiàn)log方法。之前是基于抽象類(lèi)的設(shè)計(jì)思想,編譯器會(huì)強(qiáng)制要求子類(lèi)重寫(xiě)父類(lèi)的log方法,否則就會(huì)報(bào)編譯錯(cuò)誤。
- Logger可以被實(shí)例化,這也就意味著這個(gè)空的log方法有可能會(huì)被調(diào)用。這就增加了類(lèi)被誤用的風(fēng)險(xiǎn)。當(dāng)然,這個(gè)問(wèn)題 可以通過(guò)設(shè)置私有的構(gòu)造函數(shù)的方式來(lái)解決,但是不如抽象類(lèi)優(yōu)雅。
抽象類(lèi)更多是為了代碼復(fù)用,而接口更側(cè)重于解耦。接口是對(duì)行為的一種抽象,相當(dāng)于一組協(xié)議或者契約(可類(lèi)比API接口)。調(diào)用者只需要關(guān)心抽象的接口,不需要了解具體的實(shí)現(xiàn),具體的實(shí)現(xiàn)代碼對(duì)調(diào)用者透明。接口實(shí)現(xiàn)了約定和實(shí)現(xiàn)相分離,可以降低代碼間的耦合,提高代碼的可擴(kuò)展性。
實(shí)際上,接口是一個(gè)比抽象類(lèi)應(yīng)用更加廣泛、更加重要的知識(shí)點(diǎn)。比如,我們經(jīng)常提到的 ”基于接口而非實(shí)現(xiàn)編程“ ,就是一條幾乎天天會(huì)用到的,并且能極大的提高代碼的靈活性、擴(kuò)展性的設(shè)計(jì)思想。
如何模擬抽象類(lèi)和接口
在前面列舉的例子中,我們使用Java的接口實(shí)現(xiàn)了Filter過(guò)濾器。不過(guò),在 C++ 中只提供了抽象類(lèi),并沒(méi)有提供接口,那從代碼的角度上說(shuō),是不是就無(wú)法實(shí)現(xiàn) Filter 的設(shè)計(jì)思路了呢? 大家可以先思考下 ?? ~
我們先會(huì)議下接口的定義:接口中沒(méi)有成員變量,只有方法聲明,沒(méi)有方法實(shí)現(xiàn),實(shí)現(xiàn)接口的類(lèi)必須實(shí)現(xiàn)接口中的所有方法。主要滿(mǎn)足以上幾點(diǎn)從設(shè)計(jì)的角度上來(lái)說(shuō),我們就可以把他叫做接口。
實(shí)際上,要滿(mǎn)足接口的這些特性并不難。下面我們來(lái)看下實(shí)現(xiàn):
class Strategy {
public:
-Strategy();
virtual void algorithm()=0;
protected:
Strategy();
}
抽象類(lèi) Strategy 沒(méi)有定義任何屬性,并且所有的方法都聲明為 virtual 類(lèi)型(等同于Java中的abstract關(guān)鍵字),這樣,所有的方法都不能有代碼實(shí)現(xiàn),并且所有繼承了這個(gè)抽象類(lèi)的子類(lèi),都要實(shí)現(xiàn)這些方法。從語(yǔ)法特性上看,這個(gè)抽象類(lèi)就相當(dāng)于一個(gè)接口。
處理用抽象類(lèi)來(lái)模擬接口外,我們還可以用普通類(lèi)來(lái)模擬接口。具體的Java實(shí)現(xiàn)如下所示:
public class MockInterface {
protected MockInteface();
public void funcA() {
throw new MethodUnSupportedException();
}
}
我們知道類(lèi)中的方法必須包含實(shí)現(xiàn),這個(gè)不符合接口的定義。但是,我們可以讓類(lèi)中的方法拋出 MethodUnSupportedException 異常,來(lái)模擬不包含實(shí)現(xiàn)的接口,并且強(qiáng)迫子類(lèi)來(lái)繼承這個(gè)父類(lèi)的時(shí)候,都主動(dòng)實(shí)現(xiàn)父類(lèi)的方法,否則就會(huì)在運(yùn)行時(shí)拋出異常。
那又如何避免這個(gè)類(lèi)被實(shí)例化呢? 實(shí)際上很簡(jiǎn)單,我們只需要將這個(gè)類(lèi)的構(gòu)造函數(shù)聲明為 protected 訪(fǎng)問(wèn)權(quán)限就可以了。
如何決定該用抽象還是接口?
上面的講解可能偏理論,現(xiàn)在我們就從真實(shí)項(xiàng)目開(kāi)發(fā)的角度來(lái)看下。在代碼設(shè)計(jì)/編程時(shí),什么時(shí)候該用接口?什么時(shí)候該用抽象類(lèi)?
實(shí)際上,判斷的標(biāo)準(zhǔn)很簡(jiǎn)單。如果我們需要一種is-a關(guān)系,并且是為了解決代碼復(fù)用的問(wèn)題,就用抽象類(lèi)。如果我們需要的是一種has-a關(guān)系,并且是為了解決抽象而非代碼復(fù)用問(wèn)題,我們就用接口。
從類(lèi)的繼承層次來(lái)看,抽象類(lèi)是一種自下而上的設(shè)計(jì)思路,先有子類(lèi)的代碼復(fù)用,然后再抽象成上層的父類(lèi)(也就是抽象類(lèi))。而接口則相反,它是一種自上而下的設(shè)計(jì)思路,我們?cè)诰幊痰臅r(shí)候,一般都是先設(shè)計(jì)接口,再去思考具體實(shí)現(xiàn)。
好了,你是否掌握了上面的內(nèi)容呢。你可以通過(guò)一下幾個(gè)維度來(lái)回顧自檢一下:
- 抽象類(lèi)和接口的語(yǔ)法特性
- 抽象類(lèi)和接口存在的意義
- 抽象類(lèi)和接口的應(yīng)用場(chǎng)景有哪些
以上就是 Java 抽象類(lèi)和接口的區(qū)別的詳細(xì)內(nèi)容,想要了解更多關(guān)于 Java 抽象類(lèi)和接口的其他資料,可以搜索W3Cschool其它相關(guān)技術(shù)文章!也希望大家能夠多多關(guān)注和支持我們!