03.接口vs抽象類比較

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)

https://yccoding.com/

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)看一下,抽象類具有哪些特性。

  1. 抽象類不允許被實(shí)例化,只能被繼承。也就是說(shuō),你不能 new 一個(gè)抽象類的對(duì)象出來(lái)(Logger logger = new Logger(…); 會(huì)報(bào)編譯錯(cuò)誤)。
  2. 抽象類可以包含屬性和方法。方法既可以包含代碼實(shí)現(xiàn)(比如 Logger 中的 log() 方法),也可以不包含代碼實(shí)現(xiàn)(比如 Logger 中的 doLog() 方法)。不包含代碼實(shí)現(xiàn)的方法叫作抽象方法。
  3. 子類繼承抽象類,必須實(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)原因。

  1. 在 Logger 中定義一個(gè)空的方法,會(huì)影響代碼的可讀性。如果我們不熟悉 Logger 背后的設(shè)計(jì)思想,代碼注釋又不怎么給力,我們?cè)陂喿x Logger 代碼的時(shí)候,就可能對(duì)為什么定義一個(gè)空的 log() 方法而感到疑惑,需要查看 Logger、FileLogger、MessageQueueLogger 之間的繼承關(guān)系,才能弄明白其設(shè)計(jì)意圖。
  2. 當(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() 方法,也不是不可能的。
  3. 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)單。

  1. 如果我們要表示一種 is-a 的關(guān)系,并且是為了解決代碼復(fù)用的問(wèn)題,就用抽象類;
  2. 如果我們要表示一種 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ū)別:

  1. 實(shí)現(xiàn)方式:抽象類通過(guò)繼承的方式被子類實(shí)現(xiàn),而接口通過(guò)實(shí)現(xiàn)的方式被類實(shí)現(xiàn)。一個(gè)類只能繼承一個(gè)抽象類,但可以實(shí)現(xiàn)多個(gè)接口。
  2. 方法實(shí)現(xiàn):抽象類可以包含具體方法的實(shí)現(xiàn),而接口只能包含方法的聲明,沒(méi)有具體的實(shí)現(xiàn)。類繼承抽象類時(shí),可以直接繼承具體方法的實(shí)現(xiàn),而實(shí)現(xiàn)接口時(shí),必須提供方法的具體實(shí)現(xiàn)。
  3. 關(guān)注點(diǎn):抽象類更適合用于描述一種 "是什么" 的關(guān)系,即類與類之間的繼承關(guān)系。接口更適合用于描述一種 "能做什么" 的關(guān)系,即類具有哪些方法和行為。
  4. 靈活性:抽象類可以包含實(shí)例變量,而接口只能包含常量。抽象類可以提供一些默認(rèn)的實(shí)現(xiàn),而接口只能定義方法的契約,沒(méi)有默認(rèn)實(shí)現(xiàn)。
  5. 使用場(chǎng)景:抽象類通常用于描述一組相關(guān)的類,提供一種通用的基類,而接口通常用于定義一組方法的契約,用于實(shí)現(xiàn)多態(tài)性和解耦合。

6.3 一些具體的例子

那么在實(shí)際開(kāi)發(fā)中應(yīng)該如何選擇抽象類和接口的案例:

  1. 如果需要提供一組相關(guān)類的通用行為和屬性,可以使用抽象類。比如,Android中要抽象出公共的BaseActivity,可以做到子類復(fù)用!
  2. 如果需要定義一組方法的契約,以實(shí)現(xiàn)多態(tài)性和解耦合,可以使用接口。比如,Android中要定義MVP的View和Presenter的交互接口,則要用接口!
  3. 在某些情況下,抽象類和接口可以結(jié)合使用,以滿足更復(fù)雜的設(shè)計(jì)需求。

6.4 開(kāi)發(fā)總結(jié)一下

  1. 抽象類和接口的語(yǔ)法特性

抽象類不允許被實(shí)例化,只能被繼承。它可以包含屬性和方法。方法既可以包含代碼實(shí)現(xiàn),也可以不包含代碼實(shí)現(xiàn)。不包含代碼實(shí)現(xiàn)的方法叫作抽象方法。子類繼承抽象類,必須實(shí)現(xiàn)抽象類中的所有抽象方法。接口不能包含屬性,只能聲明方法,方法不能包含代碼實(shí)現(xiàn)。類實(shí)現(xiàn)接口的時(shí)候,必須實(shí)現(xiàn)接口中聲明的所有方法。

  1. 抽象類和接口存在的意義

抽象類是對(duì)成員變量和方法的抽象,是一種 is-a 關(guān)系,是為了解決代碼復(fù)用問(wèn)題。接口僅僅是對(duì)方法的抽象,是一種 has-a 關(guān)系,表示具有某一組行為特性,是為了解決解耦問(wèn)題,隔離接口和具體的實(shí)現(xiàn),提高代碼的擴(kuò)展性。

  1. 抽象類和接口的應(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)題,那我們就用接口。

  1. 從語(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)

https://yccoding.com/

?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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