03.接口vs抽象類比較
目錄介紹
- 01.面向?qū)ο笤O(shè)計(jì)特性
- 1.1 抽象和接口特性
- 1.2 一些問(wèn)題思考
- 1.3 抽象的設(shè)計(jì)思想
- 1.4 抽象思想案例
- 02.抽象類介紹
- 2.1 看抽象案例
- 2.2 抽象類特點(diǎn)
- 2.3 抽象類設(shè)計(jì)注意要點(diǎn)
- 2.4 抽象的思想
- 2.5 模擬抽象類
- 03.接口設(shè)計(jì)介紹
- 3.1 看接口案例
- 3.2 接口的特點(diǎn)
- 3.3 接口設(shè)計(jì)注意點(diǎn)
- 3.4 接口的思想
- 3.5 Marker Interface
- 3.6 模擬接口設(shè)計(jì)
- 04.解決什么編程問(wèn)題
- 4.1 抽象類的由來(lái)
- 4.2 接口的由來(lái)
- 05.抽象類VS接口
- 5.1 理解抽象和接口
- 5.2 語(yǔ)法上不同
- 5.3 編程角度不同
- 5.4 通俗理解兩者區(qū)別
- 5.5 設(shè)計(jì)層次上區(qū)別
- 06.如何選擇場(chǎng)景
- 6.1 判斷標(biāo)準(zhǔn)很簡(jiǎn)單
- 6.2 場(chǎng)景上的區(qū)別
- 6.3 一些具體的例子
- 6.4 開(kāi)發(fā)總結(jié)一下
推薦一個(gè)好玩網(wǎng)站
一個(gè)最純粹的技術(shù)分享網(wǎng)站,打造精品技術(shù)編程專欄!編程進(jìn)階網(wǎng)
01.面向?qū)ο笤O(shè)計(jì)特性
1.1 抽象和接口特性
在面向?qū)ο缶幊讨?,抽象類和接口是兩個(gè)經(jīng)常被用到的語(yǔ)法概念,是面向?qū)ο笏拇筇匦?,以及很多設(shè)計(jì)模式、設(shè)計(jì)思想、設(shè)計(jì)原則編程實(shí)現(xiàn)的基礎(chǔ)。
比如,我們可以使用接口來(lái)實(shí)現(xiàn)面向?qū)ο蟮某橄筇匦?、多態(tài)特性和基于接口而非實(shí)現(xiàn)的設(shè)計(jì)原則,使用抽象類來(lái)實(shí)現(xiàn)面向?qū)ο蟮睦^承特性和模板設(shè)計(jì)模式等等。
并不是所有的面向?qū)ο缶幊陶Z(yǔ)言都支持這兩個(gè)語(yǔ)法概念,比如,C++ 這種編程語(yǔ)言只支持抽象類,不支持接口;而像 Python 這樣的動(dòng)態(tài)編程語(yǔ)言,既不支持抽象類,也不支持接口。
盡管有些編程語(yǔ)言沒(méi)有提供現(xiàn)成的語(yǔ)法來(lái)支持接口和抽象類,我們?nèi)匀豢梢酝ㄟ^(guò)一些手段來(lái)模擬實(shí)現(xiàn)這兩個(gè)語(yǔ)法概念。
1.2 一些問(wèn)題思考
這兩個(gè)語(yǔ)法概念不僅在工作中經(jīng)常會(huì)被用到,在面試中也經(jīng)常被提及。比如,“接口和抽象類的區(qū)別是什么?什么時(shí)候用接口?什么時(shí)候用抽象類?抽象類和接口存在的意義是什么?能解決哪些編程問(wèn)題?”等等。
1.3 抽象的設(shè)計(jì)思想
抽象思想是指將事物或概念從具體的、特定的細(xì)節(jié)中抽離出來(lái),關(guān)注其普遍性、共性和本質(zhì)特征的思維方式。它是一種對(duì)事物進(jìn)行概括、歸納和提煉的思考方式,通過(guò)忽略細(xì)節(jié)和個(gè)別差異,抓住事物的本質(zhì)和共同點(diǎn),以更高層次的概念和模型來(lái)理解和描述事物。
1.4 抽象思想案例
代碼如下所示。抽象類除了有抽象類特性之外,還可以解決代碼復(fù)用問(wèn)題。
/*抽象類作為參數(shù)的時(shí)候如何進(jìn)行調(diào)用*/
abstract class Animal {
protected int x;
private int y;
// 定義一個(gè)抽象方法
public abstract void eat() ;
public void func2() {
System.out.println("func2");
}
}
// 定義一個(gè)類,貓
class Cat extends Animal {
public void eat(){
System.out.println("吃魚(yú).................") ;
}
}
// 定義一個(gè)類,狗
class Dog extends Animal {
public void eat(){
System.out.println("吃骨頭.................") ;
}
}
// 定義一個(gè)類,動(dòng)物類
class AnimalDemo {
public void method(Animal a) {
a.eat() ;
}
}
// 測(cè)試類
class ArgsDemo2 {
public static void main(String[] args) {
// 創(chuàng)建AnimalDemo的對(duì)象
AnimalDemo ad = new AnimalDemo() ;
// 對(duì)Animal進(jìn)行間接實(shí)例化
// Animal a = new Cat() ;
// Animal a = new Dog() ;
Cat a = new Cat() ;
// 調(diào)用method方法
ad.method(a) ;
}
}
02.抽象類介紹
2.1 看抽象案例
不同的編程語(yǔ)言對(duì)接口和抽象類的定義方式可能有些差別,但差別并不會(huì)很大。首先來(lái)看一下,在 Java 這種編程語(yǔ)言中,我們是如何定義抽象類的。
下面這段代碼是一個(gè)比較典型的抽象類的使用場(chǎng)景(模板設(shè)計(jì)模式)。
- Logger 是一個(gè)記錄日志的抽象類,F(xiàn)ileLogger 和 MessageQueueLogger 繼承 Logger,分別實(shí)現(xiàn)兩種不同的日志記錄方式:記錄日志到文件中和記錄日志到消息隊(duì)列中。
- FileLogger 和 MessageQueueLogger 兩個(gè)子類復(fù)用了父類方法,但因?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.writer = 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(...);
}
}
2.2 抽象類特點(diǎn)
抽象類是一種在面向?qū)ο缶幊讨械母拍睿遣荒鼙粚?shí)例化的類,只能被繼承。抽象類用于定義一組抽象方法和可能的具體方法,以提供一種通用的接口和行為,供其子類實(shí)現(xiàn)和繼承。
通過(guò)上面的這個(gè)例子來(lái)看一下,抽象類具有哪些特性。
- 抽象類不允許被實(shí)例化,只能被繼承。也就是說(shuō),你不能 new 一個(gè)抽象類的對(duì)象出來(lái)(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() 方法。
抽象類提供了一種抽象的概念和接口,用于定義一組相關(guān)的類的共同行為和屬性。它可以作為一種模板或基類,為子類提供一致的接口和行為,實(shí)現(xiàn)代碼的復(fù)用和多態(tài)性。
2.3 抽象類設(shè)計(jì)注意要點(diǎn)
如果想要設(shè)計(jì)這樣一個(gè)類,該類包含一個(gè)特別的成員方法,方法的具體實(shí)現(xiàn)由它的子類確定,那么可以在父類中聲明該方法為抽象方法
Abstract關(guān)鍵字同樣可以用來(lái)聲明抽象方法,抽象方法只包含一個(gè)方法名,而沒(méi)有方法體。聲明抽象方法會(huì)造成以下兩個(gè)結(jié)果:
- 如果一個(gè)類包含抽象方法,則該類必須聲明為抽象類
- 子類必須重寫父類的抽象方法,否則自身也必須聲明為抽象類
2.4 抽象的思想
抽象特性的定義講完了,我們?cè)賮?lái)看一下,抽象的意義是什么?它能解決什么編程問(wèn)題?
實(shí)際上,如果上升一個(gè)思考層面的話,抽象及其前面講到的封裝都是人類處理復(fù)雜性的有效手段。
在面對(duì)復(fù)雜系統(tǒng)的時(shí)候,人腦能承受的信息復(fù)雜程度是有限的,所以我們必須忽略掉一些非關(guān)鍵性的實(shí)現(xiàn)細(xì)節(jié)。抽象作為一種只關(guān)注功能點(diǎn)不關(guān)注實(shí)現(xiàn)的設(shè)計(jì)思路,正好幫我們的大腦過(guò)濾掉許多非必要的信息。
抽象作為一個(gè)非常寬泛的設(shè)計(jì)思想,很多設(shè)計(jì)原則都體現(xiàn)了抽象這種設(shè)計(jì)思想,比如基于接口而非實(shí)現(xiàn)編程、開(kāi)閉原則(對(duì)擴(kuò)展開(kāi)放、對(duì)修改關(guān)閉)、代碼解耦(降低代碼的耦合性)等。
舉個(gè)簡(jiǎn)單例子,比如 getAliPictureUrl() 就不是一個(gè)具有抽象思維的命名,因?yàn)槟骋惶烊绻覀儾辉侔褕D片存儲(chǔ)在阿里云上,而是存儲(chǔ)在私有云上,那這個(gè)命名也要隨之被修改。相反,如果我們定義一個(gè)比較抽象的函數(shù),比如叫作 getPictureUrl(),那即便內(nèi)部存儲(chǔ)方式修改了,我們也不需要修改命名。
2.5 模擬抽象類
在 Python、Ruby 這些動(dòng)態(tài)語(yǔ)言中,不僅沒(méi)有接口的概念,也沒(méi)有類似 abstract、virtual 這樣的關(guān)鍵字來(lái)定義抽象類,那該如何實(shí)現(xiàn)上面的講到的 抽象類 的設(shè)計(jì)思路呢?
實(shí)際上,除了用抽象類來(lái)模擬接口之外,還可以用普通類來(lái)模擬接口。具體的 Java 代碼實(shí)現(xiàn)如下所示。
public class MockInteface {
protected MockInteface() {}
public void funcA() {
throw new MethodUnSupportedException();
}
}
類中的方法必須包含實(shí)現(xiàn),這個(gè)不符合接口的定義。但是,我們可以讓類中的方法拋出 MethodUnSupportedException 異常,來(lái)模擬不包含實(shí)現(xiàn)的接口,并且能強(qiáng)迫子類在繼承這個(gè)父類的時(shí)候,都去主動(dòng)實(shí)現(xiàn)父類的方法,否則就會(huì)在運(yùn)行時(shí)拋出異常。
那又如何避免這個(gè)類被實(shí)例化呢?實(shí)際上很簡(jiǎn)單,我們只需要將這個(gè)類的構(gòu)造函數(shù)聲明為 protected 訪問(wèn)權(quán)限就可以了。
03.接口設(shè)計(jì)介紹
3.1 看接口案例
再來(lái)看一下,在 Java 這種編程語(yǔ)言中,我們?nèi)绾味x接口。
// 接口
public interface Filter {
void doFilter(RpcRequest req) throws RpcException;
}
// 接口實(shí)現(xiàn)類:鑒權(quán)過(guò)濾器
public class AuthencationFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...鑒權(quán)邏輯..
}
}
// 接口實(shí)現(xiàn)類:限流過(guò)濾器
public class RateLimitFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...限流邏輯...
}
}
// 過(guò)濾器使用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) {
// ...處理過(guò)濾結(jié)果...
}
// ...省略其他處理邏輯...
}
}
上面這段代碼是一個(gè)比較典型的接口的使用場(chǎng)景。通過(guò) Java 中的 interface 關(guān)鍵字定義了一個(gè) Filter 接口。AuthenticationFilter 和 RateLimitFilter 是接口的兩個(gè)實(shí)現(xiàn)類,分別實(shí)現(xiàn)了對(duì) RPC 請(qǐng)求鑒權(quán)和限流的過(guò)濾功能。
3.2 接口的特點(diǎn)
代碼非常簡(jiǎn)潔。結(jié)合代碼再來(lái)看一下,接口都有哪些特性。
- 接口不能包含屬性(也就是成員變量)。
- 接口只能聲明方法,方法不能包含代碼實(shí)現(xiàn)。
- 類實(shí)現(xiàn)接口的時(shí)候,必須實(shí)現(xiàn)接口中聲明的所有方法。
接口是一種抽象的概念,用于定義一組方法的契約,而不涉及具體的實(shí)現(xiàn)。接口定義了類應(yīng)該具有的方法和行為,以提供一種通用的接口,供類來(lái)實(shí)現(xiàn)。
3.3 接口設(shè)計(jì)注意點(diǎn)
3.4 接口的思想
3.5 Marker Interface
接口的職責(zé)也不僅僅限于抽象方法的集合,其實(shí)有各種不同的實(shí)踐。
有一類沒(méi)有任何方法的接口,通常叫作 Marker Interface,顧名思義,它的目的就是為了聲明某些東西,比如我們熟知的 Cloneable、Serializable 等。這種用法,也存在于業(yè)界其他的 Java 產(chǎn)品代碼中。
3.6 模擬接口設(shè)計(jì)
如果你熟悉的是 C++ 這種編程語(yǔ)言,你可能會(huì)說(shuō),C++ 只有抽象類,并沒(méi)有接口,那從代碼實(shí)現(xiàn)的角度上來(lái)說(shuō),是不是就無(wú)法實(shí)現(xiàn) 接口 的設(shè)計(jì)思路了呢?
先來(lái)回憶一下接口的定義:接口中沒(méi)有成員變量,只有方法聲明,沒(méi)有方法實(shí)現(xiàn),實(shí)現(xiàn)接口的類必須實(shí)現(xiàn)接口中的所有方法。
只要滿足這樣幾點(diǎn),從設(shè)計(jì)的角度上來(lái)說(shuō),我們就可以把它叫作接口。實(shí)際上,要滿足接口的這些語(yǔ)法特性并不難。
在下面這段 C++ 代碼中,就用抽象類模擬了一個(gè)接口(下面這段代碼實(shí)際上是策略模式中的一段代碼)。
class Strategy { // 用抽象類模擬接口
public:
~Strategy();
virtual void algorithm()=0;
protected:
Strategy();
};
抽象類 Strategy 沒(méi)有定義任何屬性,并且所有的方法都聲明為 virtual 類型(等同于 Java 中的 abstract 關(guān)鍵字)。
這樣,所有的方法都不能有代碼實(shí)現(xiàn),并且所有繼承這個(gè)抽象類的子類,都要實(shí)現(xiàn)這些方法。從語(yǔ)法特性上來(lái)看,這個(gè)抽象類就相當(dāng)于一個(gè)接口。
04.解決什么編程問(wèn)題
4.1 抽象類的由來(lái)
抽象類也是為代碼復(fù)用而生的。多個(gè)子類可以繼承抽象類中定義的屬性和方法,避免在子類中,重復(fù)編寫相同的代碼。
不過(guò),既然繼承本身就能達(dá)到代碼復(fù)用的目的,而繼承也并不要求父類一定是抽象類,那我們不使用抽象類,照樣也可以實(shí)現(xiàn)繼承和復(fù)用。從這個(gè)角度上來(lái)講,我們貌似并不需要抽象類這種語(yǔ)法呀。
那抽象類除了解決代碼復(fù)用的問(wèn)題,還有什么其他存在的意義嗎?
還是拿之前那個(gè)打印日志的例子。我們先對(duì)上面的代碼做下改造。在改造之后的代碼中,Logger 不再是抽象類,只是一個(gè)普通的父類,刪除了 Logger 中 log()、doLog() 方法,新增了 isLoggable() 方法。FileLogger 和 MessageQueueLogger 還是繼承 Logger 父類,以達(dá)到代碼復(fù)用的目的。具體的代碼如下:
// 父類:非抽象類,就是普通的類. 刪除了log(),doLog(),新增了isLoggable().
public class Logger {
private String name;
private boolean enabled;
private Level minPermittedLevel;
public Logger(String name, boolean enabled, Level minPermittedLevel) {
//...構(gòu)造函數(shù)不變,代碼省略...
}
protected boolean isLoggable() {
boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
return loggable;
}
}
// 子類:輸出日志到文件
public class FileLogger extends Logger {
private Writer fileWriter;
public FileLogger(String name, boolean enabled,
Level minPermittedLevel, String filepath) {
//...構(gòu)造函數(shù)不變,代碼省略...
}
public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,輸出到日志文件
fileWriter.write(...);
}
}
// 子類: 輸出日志到消息中間件(比如kafka)
public class MessageQueueLogger extends Logger {
private MessageQueueClient msgQueueClient;
public MessageQueueLogger(String name, boolean enabled,
Level minPermittedLevel, MessageQueueClient msgQueueClient) {
//...構(gòu)造函數(shù)不變,代碼省略...
}
public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,輸出到消息中間件
msgQueueClient.send();
}
}
這個(gè)設(shè)計(jì)思路雖然達(dá)到了代碼復(fù)用的目的,但是無(wú)法使用多態(tài)特性了。像下面這樣編寫代碼,就會(huì)出現(xiàn)編譯錯(cuò)誤,因?yàn)?Logger 中并沒(méi)有定義 log() 方法。
Logger logger = new FileLogger("access-log", true, Level.WARN, "/users/yc/access.log");
logger.log(Level.ERROR, "This is a test log message.");
你可能會(huì)說(shuō),這個(gè)問(wèn)題解決起來(lái)很簡(jiǎn)單啊。在 Logger 父類中,定義一個(gè)空的 log() 方法,讓子類重寫父類的 log() 方法,實(shí)現(xiàn)自己的記錄日志的邏輯,不就可以了嗎?
public class Logger {
// ...省略部分代碼...
public void log(Level level, String mesage) { // do nothing... }
}
public class FileLogger extends Logger {
// ...省略部分代碼...
@Override
public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,輸出到日志文件
fileWriter.write(...);
}
}
public class MessageQueueLogger extends Logger {
// ...省略部分代碼...
@Override
public void log(Level level, String mesage) {
if (!isLoggable()) return;
// 格式化level和message,輸出到消息中間件
msgQueueClient.send(...);
}
}
這個(gè)設(shè)計(jì)思路能用,但是,它顯然沒(méi)有之前通過(guò)抽象類的實(shí)現(xiàn)思路優(yōu)雅。為什么這么說(shuō)呢?主要有以下幾點(diǎn)原因。
- 在 Logger 中定義一個(gè)空的方法,會(huì)影響代碼的可讀性。如果我們不熟悉 Logger 背后的設(shè)計(jì)思想,代碼注釋又不怎么給力,我們?cè)陂喿x Logger 代碼的時(shí)候,就可能對(duì)為什么定義一個(gè)空的 log() 方法而感到疑惑,需要查看 Logger、FileLogger、MessageQueueLogger 之間的繼承關(guān)系,才能弄明白其設(shè)計(jì)意圖。
- 當(dāng)創(chuàng)建一個(gè)新的子類繼承 Logger 父類的時(shí)候,我們有可能會(huì)忘記重新實(shí)現(xiàn) log() 方法。之前基于抽象類的設(shè)計(jì)思路,編譯器會(huì)強(qiáng)制要求子類重寫 log() 方法,否則會(huì)報(bào)編譯錯(cuò)誤。我們舉的例子比較簡(jiǎn)單,Logger 中的方法不多,代碼行數(shù)也很少。但是,如果 Logger 有幾百行,有 n 多方法,除非你對(duì) Logger 的設(shè)計(jì)非常熟悉,否則忘記重新實(shí)現(xiàn) log() 方法,也不是不可能的。
- Logger 可以被實(shí)例化,換句話說(shuō),我們可以 new 一個(gè) Logger 出來(lái),并且調(diào)用空的 log() 方法。這也增加了類被誤用的風(fēng)險(xiǎn)。當(dāng)然,這個(gè)問(wèn)題可以通過(guò)設(shè)置私有的構(gòu)造函數(shù)的方式來(lái)解決。不過(guò),顯然沒(méi)有通過(guò)抽象類來(lái)的優(yōu)雅。
4.2 接口的由來(lái)
為什么需要接口?它能夠解決什么編程問(wèn)題?
抽象類更多的是為了代碼復(fù)用,而接口就更側(cè)重于解耦。接口是對(duì)行為的一種抽象,相當(dāng)于一組協(xié)議或者契約,調(diào)用者只需要關(guān)注抽象的接口,不需要了解具體的實(shí)現(xiàn),具體的實(shí)現(xiàn)代碼對(duì)調(diào)用者透明。接口實(shí)現(xiàn)了約定和實(shí)現(xiàn)相分離,可以降低代碼間的耦合性,提高代碼的可擴(kuò)展性。
實(shí)際上,接口是一個(gè)比抽象類應(yīng)用更加廣泛、更加重要的知識(shí)點(diǎn)。比如,經(jīng)常提到的“基于接口而非實(shí)現(xiàn)編程”,就是一條幾乎天天會(huì)用到,并且能極大地提高代碼的靈活性、擴(kuò)展性的設(shè)計(jì)思想。
05.抽象類VS接口
5.1 理解抽象和接口
這兩個(gè)語(yǔ)法概念不僅在工作中經(jīng)常會(huì)被用到,在面試中也經(jīng)常被提及。比如,“接口和抽象類的區(qū)別是什么?什么時(shí)候用接口?什么時(shí)候用抽象類?抽象類和接口存在的意義是什么?能解決哪些編程問(wèn)題?”等等。
abstract class和interface之間在對(duì)于抽象類定義的支持方面具有很大的相似性,甚至可以相互替換,避免使用時(shí)在進(jìn)行抽象類定義時(shí)對(duì)于 abstract class和interface的選擇隨意。
其實(shí),兩者之間還是有很大的區(qū)別的,對(duì)于它們的選擇甚至反映出對(duì)于問(wèn)題領(lǐng)域本質(zhì)的理解、對(duì)于設(shè)計(jì)意圖的理解是否正確、合理。
本文將對(duì)它們之間的區(qū)別進(jìn)行一番剖析,試圖給開(kāi)發(fā)者提供一個(gè)在二者之間進(jìn)行選擇的依據(jù)。
5.2 語(yǔ)法上不同
抽象類
abstract class Student {
abstract void method1();
abstract void method2();
public void method3() {
System.out.println("func2");
}
}
接口
interface Student {
//接口中的變量其實(shí)就是常量,默認(rèn)被final修飾
int age = 10;
void method1();
void method2();
}
在abstract class方式中,Demo可以有自己的數(shù)據(jù)成員,也可以有非abstract的成員方法,而在interface方式的實(shí)現(xiàn)中,Demo只能夠有靜態(tài)的不能被修改的數(shù)據(jù)成員(也就是必須是static final的,不過(guò)在interface中一般不定義數(shù)據(jù)成員),所有的成員方法都是abstract的。從某種意義上說(shuō),interface是一種特殊形式的abstract class。
抽象類實(shí)際上就是類,只不過(guò)是一種特殊的類,這種類不能被實(shí)例化為對(duì)象,只能被子類繼承。我們知道,繼承關(guān)系是一種 is-a 的關(guān)系,那抽象類既然屬于類,也表示一種 is-a 的關(guān)系。相對(duì)于抽象類的 is-a 關(guān)系來(lái)說(shuō),接口表示一種 has-a 關(guān)系,表示具有某些功能。對(duì)于接口,有一個(gè)更加形象的叫法,那就是協(xié)議(contract)。
兩者語(yǔ)法上的區(qū)別
- 抽象類方式中,抽象類可以擁有任意范圍的成員數(shù)據(jù),同時(shí)也可以擁有自己的非抽象方法,
- 但是接口方式中,它僅能夠有靜態(tài)、不能修改的成員數(shù)據(jù)(但是我們一般是不會(huì)在接口中使用成員數(shù)據(jù)),同時(shí)它所有的方法都必須是抽象的。
- 在某種程度上來(lái)說(shuō),接口是抽象類的特殊化。
- 對(duì)子類而言,它只能繼承一個(gè)抽象類(這是java為了數(shù)據(jù)安全而考慮的),但是卻可以實(shí)現(xiàn)多個(gè)接口。
5.3 編程角度不同
abstract class在Java語(yǔ)言中表示的是一種繼承關(guān)系,一個(gè)類只能使用一次繼承關(guān)系。但是,一個(gè)類卻可以實(shí)現(xiàn)多個(gè)interface。也許,這是Java語(yǔ)言的設(shè)計(jì)者在考慮Java對(duì)于多重繼承的支持方面的一種折中考慮吧。
其次,在abstract class的定義中,我們可以賦予方法的默認(rèn)行為。但是在interface的定義中,方法卻不能擁有默認(rèn)行為,不過(guò)在JDK1.8中可以使用default關(guān)鍵字實(shí)現(xiàn)默認(rèn)方法。
interface InterfaceA {
default void foo() {
System.out.println("InterfaceA foo");
}
}
在 Java 8 之前,接口與其實(shí)現(xiàn)類之間的 耦合度 太高了(tightly coupled),當(dāng)需要為一個(gè)接口添加方法時(shí),所有的實(shí)現(xiàn)類都必須隨之修改。默認(rèn)方法解決了這個(gè)問(wèn)題,它可以為接口添加新的方法,而不會(huì)破壞已有的接口的實(shí)現(xiàn)。這在 lambda 表達(dá)式作為Java 8 語(yǔ)言的重要特性而出現(xiàn)之際,為升級(jí)舊接口且保持向后兼容(backward compatibility)提供了途徑。
5.4 通俗理解兩者區(qū)別
接口和抽象類的概念不一樣。接口是對(duì)動(dòng)作的抽象,抽象類是對(duì)根源的抽象。從設(shè)計(jì)理念上,接口反映的是 “l(fā)ike-a” 關(guān)系,抽象類反映的是 “is-a” 關(guān)系。
抽象類表示的是,這個(gè)對(duì)象是什么。接口表示的是,這個(gè)對(duì)象能做什么。比如,男人,女人,這兩個(gè)類(如果是類的話……),他們的抽象類是人。說(shuō)明,他們都是人。
人可以吃東西,狗也可以吃東西,你可以把“吃東西”定義成一個(gè)接口,然后讓這些類去實(shí)現(xiàn)它.
所以,在高級(jí)語(yǔ)言上,一個(gè)類只能繼承一個(gè)類(抽象類)(正如人不可能同時(shí)是生物和非生物),但是可以實(shí)現(xiàn)多個(gè)接口(吃飯接口、走路接口)。
5.5 設(shè)計(jì)層次上區(qū)別
抽象層次不同:抽象類是對(duì)類抽象,而接口是對(duì)行為的抽象。抽象類是對(duì)整個(gè)類整體進(jìn)行抽象,包括屬性、行為,但是接口卻是對(duì)類局部(行為)進(jìn)行抽象。
跨域不同:抽象類所跨域的是具有相似特點(diǎn)的類,而接口卻可以跨域不同的類。我們知道抽象類是從子類中發(fā)現(xiàn)公共部分,然后泛化成抽象類,子類繼承該父類即可,但是接口不同。實(shí)現(xiàn)它的子類可以不存在任何關(guān)系,共同之處。例如貓、狗可以抽象成一個(gè)動(dòng)物類抽象類,具備叫的方法。鳥(niǎo)、飛機(jī)可以實(shí)現(xiàn)飛Fly接口,具備飛的行為,這里我們總不能將鳥(niǎo)、飛機(jī)共用一個(gè)父類吧!所以說(shuō)抽象類所體現(xiàn)的是一種繼承關(guān)系,要想使得繼承關(guān)系合理,父類和派生類之間必須存在"is-a" 關(guān)系,即父類和派生類在概念本質(zhì)上應(yīng)該是相同的。對(duì)于接口則不然,并不要求接口的實(shí)現(xiàn)者和接口定義在概念本質(zhì)上是一致的, 僅僅是實(shí)現(xiàn)了接口定義的契約而已。
設(shè)計(jì)層次不同
- 對(duì)于抽象類而言,它是自下而上來(lái)設(shè)計(jì)的,我們要先知道子類才能抽象出父類,而接口則不同,它根本就不需要知道子類的存在,只需要定義一個(gè)規(guī)則即可,至于什么子類、什么時(shí)候怎么實(shí)現(xiàn)它一概不知。比如我們只有一個(gè)貓類在這里,如果你這是就抽象成一個(gè)動(dòng)物類,是不是設(shè)計(jì)有點(diǎn)兒過(guò)度?我們起碼要有兩個(gè)動(dòng)物類,貓、狗在這里,我們?cè)诔橄笏麄兊墓餐c(diǎn)形成動(dòng)物抽象類吧!所以說(shuō)抽象類往往都是通過(guò)重構(gòu)而來(lái)的!
- 但是接口就不同,比如說(shuō)飛,我們根本就不知道會(huì)有什么東西來(lái)實(shí)現(xiàn)這個(gè)飛接口,怎么實(shí)現(xiàn)也不得而知,我們要做的就是事前定義好飛的行為接口。所以說(shuō)抽象類是自底向上抽象而來(lái)的,接口是自頂向下設(shè)計(jì)出來(lái)的。
06.如何選擇場(chǎng)景
6.1 判斷標(biāo)準(zhǔn)很簡(jiǎn)單
實(shí)際上,判斷的標(biāo)準(zhǔn)很簡(jiǎn)單。
- 如果我們要表示一種 is-a 的關(guān)系,并且是為了解決代碼復(fù)用的問(wèn)題,就用抽象類;
- 如果我們要表示一種 has-a 關(guān)系,并且是為了解決抽象而非代碼復(fù)用的問(wèn)題,那就可以使用接口。
從類的繼承層次上來(lái)看,抽象類是一種自下而上的設(shè)計(jì)思路,先有子類的代碼重復(fù),然后再抽象成上層的父類(也就是抽象類)。而接口正好相反,它是一種自上而下的設(shè)計(jì)思路。我們?cè)诰幊痰臅r(shí)候,一般都是先設(shè)計(jì)接口,再去考慮具體的實(shí)現(xiàn)。
6.2 場(chǎng)景上的區(qū)別
抽象類和接口在設(shè)計(jì)上有一些區(qū)別,盡管它們都是面向?qū)ο缶幊讨械某橄蟾拍睢R韵率撬鼈冎g的一些主要區(qū)別:
- 實(shí)現(xiàn)方式:抽象類通過(guò)繼承的方式被子類實(shí)現(xiàn),而接口通過(guò)實(shí)現(xiàn)的方式被類實(shí)現(xiàn)。一個(gè)類只能繼承一個(gè)抽象類,但可以實(shí)現(xiàn)多個(gè)接口。
- 方法實(shí)現(xiàn):抽象類可以包含具體方法的實(shí)現(xiàn),而接口只能包含方法的聲明,沒(méi)有具體的實(shí)現(xiàn)。類繼承抽象類時(shí),可以直接繼承具體方法的實(shí)現(xiàn),而實(shí)現(xiàn)接口時(shí),必須提供方法的具體實(shí)現(xiàn)。
- 關(guān)注點(diǎn):抽象類更適合用于描述一種 "是什么" 的關(guān)系,即類與類之間的繼承關(guān)系。接口更適合用于描述一種 "能做什么" 的關(guān)系,即類具有哪些方法和行為。
- 靈活性:抽象類可以包含實(shí)例變量,而接口只能包含常量。抽象類可以提供一些默認(rèn)的實(shí)現(xiàn),而接口只能定義方法的契約,沒(méi)有默認(rèn)實(shí)現(xiàn)。
- 使用場(chǎng)景:抽象類通常用于描述一組相關(guān)的類,提供一種通用的基類,而接口通常用于定義一組方法的契約,用于實(shí)現(xiàn)多態(tài)性和解耦合。
6.3 一些具體的例子
那么在實(shí)際開(kāi)發(fā)中應(yīng)該如何選擇抽象類和接口的案例:
- 如果需要提供一組相關(guān)類的通用行為和屬性,可以使用抽象類。比如,Android中要抽象出公共的BaseActivity,可以做到子類復(fù)用!
- 如果需要定義一組方法的契約,以實(shí)現(xiàn)多態(tài)性和解耦合,可以使用接口。比如,Android中要定義MVP的View和Presenter的交互接口,則要用接口!
- 在某些情況下,抽象類和接口可以結(jié)合使用,以滿足更復(fù)雜的設(shè)計(jì)需求。
6.4 開(kāi)發(fā)總結(jié)一下
- 抽象類和接口的語(yǔ)法特性
抽象類不允許被實(shí)例化,只能被繼承。它可以包含屬性和方法。方法既可以包含代碼實(shí)現(xiàn),也可以不包含代碼實(shí)現(xiàn)。不包含代碼實(shí)現(xiàn)的方法叫作抽象方法。子類繼承抽象類,必須實(shí)現(xiàn)抽象類中的所有抽象方法。接口不能包含屬性,只能聲明方法,方法不能包含代碼實(shí)現(xiàn)。類實(shí)現(xiàn)接口的時(shí)候,必須實(shí)現(xiàn)接口中聲明的所有方法。
- 抽象類和接口存在的意義
抽象類是對(duì)成員變量和方法的抽象,是一種 is-a 關(guān)系,是為了解決代碼復(fù)用問(wèn)題。接口僅僅是對(duì)方法的抽象,是一種 has-a 關(guān)系,表示具有某一組行為特性,是為了解決解耦問(wèn)題,隔離接口和具體的實(shí)現(xiàn),提高代碼的擴(kuò)展性。
- 抽象類和接口的應(yīng)用場(chǎng)景區(qū)別
什么時(shí)候該用抽象類?什么時(shí)候該用接口?實(shí)際上,判斷的標(biāo)準(zhǔn)很簡(jiǎn)單。如果要表示一種 is-a 的關(guān)系,并且是為了解決代碼復(fù)用問(wèn)題,我們就用抽象類;如果要表示一種 has-a 關(guān)系,并且是為了解決抽象而非代碼復(fù)用問(wèn)題,那我們就用接口。
- 從語(yǔ)法特性上對(duì)比,這兩者有比較大的區(qū)別:
語(yǔ)法特性的區(qū)別:比如抽象類中可以定義屬性、方法的實(shí)現(xiàn),而接口中不能定義屬性,方法也不能包含代碼實(shí)現(xiàn)等等。
設(shè)計(jì)角度的區(qū)別:抽象類實(shí)際上就是類,只不過(guò)是一種特殊的類,這種類不能被實(shí)例化為對(duì)象,只能被子類繼承。我們知道,繼承關(guān)系是一種 is-a 的關(guān)系,那抽象類既然屬于類,也表示一種 is-a 的關(guān)系。相對(duì)于抽象類的 is-a 關(guān)系來(lái)說(shuō),接口表示一種 has-a 關(guān)系,表示具有某些功能。對(duì)于接口,有一個(gè)更加形象的叫法,那就是協(xié)議(contract)。
07.更多內(nèi)容推薦
| 模塊 | 描述 | 備注 |
|---|---|---|
| GitHub | 多個(gè)YC系列開(kāi)源項(xiàng)目,包含Android組件庫(kù),以及多個(gè)案例 | GitHub |
| 博客匯總 | 匯聚Java,Android,C/C++,網(wǎng)絡(luò)協(xié)議,算法,編程總結(jié)等 | YCBlogs |
| 設(shè)計(jì)模式 | 六大設(shè)計(jì)原則,23種設(shè)計(jì)模式,設(shè)計(jì)模式案例,面向?qū)ο笏枷?/td> | 設(shè)計(jì)模式 |
| Java進(jìn)階 | 數(shù)據(jù)設(shè)計(jì)和原理,面向?qū)ο蠛诵乃枷?,IO,異常,線程和并發(fā),JVM | Java高級(jí) |
| 網(wǎng)絡(luò)協(xié)議 | 網(wǎng)絡(luò)實(shí)際案例,網(wǎng)絡(luò)原理和分層,Https,網(wǎng)絡(luò)請(qǐng)求,故障排查 | 網(wǎng)絡(luò)協(xié)議 |
| 計(jì)算機(jī)原理 | 計(jì)算機(jī)組成結(jié)構(gòu),框架,存儲(chǔ)器,CPU設(shè)計(jì),內(nèi)存設(shè)計(jì),指令編程原理,異常處理機(jī)制,IO操作和原理 | 計(jì)算機(jī)基礎(chǔ) |
| 學(xué)習(xí)C編程 | C語(yǔ)言入門級(jí)別系統(tǒng)全面的學(xué)習(xí)教程,學(xué)習(xí)三到四個(gè)綜合案例 | C編程 |
| C++編程 | C++語(yǔ)言入門級(jí)別系統(tǒng)全面的教學(xué)教程,并發(fā)編程,核心原理 | C++編程 |
| 算法實(shí)踐 | 專欄,數(shù)組,鏈表,棧,隊(duì)列,樹(shù),哈希,遞歸,查找,排序等 | Leetcode |
| Android | 基礎(chǔ)入門,開(kāi)源庫(kù)解讀,性能優(yōu)化,F(xiàn)ramework,方案設(shè)計(jì) | Android |
推薦一個(gè)好玩網(wǎng)站
一個(gè)最純粹的技術(shù)分享網(wǎng)站,打造精品技術(shù)編程專欄!編程進(jìn)階網(wǎng)