搞了這么多年終於知道接口和抽象類的應用場景了


一. 對接口的三個疑問

很多初學者都大概清楚interface是什么, 我們可以定義1個接口, 然后在里面定義一兩個常量(static final) 或抽象方法.

然后以后寫的類就可以實現這個接口, 重寫里面的抽象方法.

很多人說接口通常跟多態性一起存在.

接口的用法跟抽象類有點類似.

但是為何要這么做呢.

  1. 為什么不直接在類里面寫對應的方法,  而要多寫1個接口(或抽象類)?

  2. 既然接口跟抽象類差不多, 什么情況下要用接口而不是抽象類.

  3. 為什么interface叫做接口呢? 跟一般范疇的接口例如usb接口, 顯卡接口有什么聯系呢?

二. 接口引用可以指向實現該接口的對象

我們清楚接口是不可以被實例化, 但是接口引用可以指向1個實現該接口的對象.

也就是說.

假如類A impletments 了接口B

那么下面是合法的:

B b = new A();

也可以把A的對象強制轉換為 接口B的對象

A a = new A90;
B b = (B)a;

這個特性是下面內容的前提.推薦:一百期面試題匯總

三. 抽象類為了多態的實現.

第1個答案十分簡單, 就是為了實現多態.

下面用詳細代碼舉1個例子.

先定義幾個類,

  • 動物(Animal) 抽象類

  • 爬行動物(Reptile) 抽象類  繼承動物類

  • 哺乳動物(Mammal) 抽象類 繼承動物類

  • 山羊(Goat) 繼承哺乳動物類

  • 老虎(Tiger)  繼承哺乳動物類

  • 兔子(Rabbit) 繼承哺乳動物類

  • 蛇(Snake)   繼承爬行動物類

  • 農夫(Farmer)   沒有繼承任何類 但是農夫可以給Animal喂水(依賴關系)

3.1 Animal類

這個是抽象類, 顯示也沒有"動物" 這種實體

類里面包含3個抽象方法.

1.靜態方法getName()

2.移動方法move(), 因為動物都能移動.  但是各種動物有不同的移動方法, 例如老虎和山羊會跑着移動, 兔子跳着移動, 蛇會爬着移動.

作為抽象基類, 我們不關心繼承的實體類是如何移動的, 所以移動方法move()是1個抽象方法.  這個就是多態的思想.

3.喝水方法drink(), 同樣, 各種動物有各種飲水方法. 這個也是抽象方法.

代碼:

abstract class Animal{
    public abstract String getName();
    public abstract void move(String destination);
    public abstract void drink();
}

3.2 Mammal類

這個是繼承動物類的哺乳動物類, 后面的老虎山羊等都繼承自這個類.

Mammal類自然繼承了Animal類的3個抽象方法, 實體類不再用寫其他代碼.

abstract class Mammal extends Animal{

}

3.3 Reptile類

這個是代表爬行動物的抽象類, 同上, 都是繼承自Animal類.

abstract class Reptile extends Animal{
 
}

3.4 Tiger類

老虎類就是1個實體類, 所以它必須重寫所有繼承自超類的抽象方法, 至於那些方法如何重寫, 則取決於老虎類本身.

class Tiger extends Mammal{
    private static String name = "Tiger";
    public String getName(){
        return this.name;
    }
 
    public void move(String destination){
        System.out.println("Goat moved to " + destination + ".");
    }
 
    public void drink(){
        System.out.println("Goat lower it's head and drink.");
    }
}

如上, 老虎的移動方法很普通, 低頭喝水.

3.5 Goat類 和 Rabbit類

這個兩個類與Tiger類似, 它們都繼承自Mammal這個類.

class Goat extends Mammal{
    private static String name = "Goat";
    public String getName(){
        return this.name;
    }
 
    public void move(String destination){
        System.out.println("Goat moved to " + destination + ".");
    }
 
    public void drink(){
        System.out.println("Goat lower it's head and drink.");
    }
}

兔子: 喝水方法有點區別

class Rabbit extends Mammal{
    private static String name = "Rabbit";
    public String getName(){
        return this.name;
    }
 
    public void move(String destination){
        System.out.println("Rabbit moved to " + destination + ".");
    }
 
    public void drink(){
        System.out.println("Rabbit put out it's tongue and drink.");
    }
}

3.6 Snake類

蛇類繼承自Reptile(爬行動物)

移動方法和喝水方法都跟其他3動物有點區別.

class Snake extends Reptile{
    private static String name = "Snake";
    public String getName(){
        return this.name;
    }
 
    public void move(String destination){
        System.out.println("Snake crawled to " + destination + ".");
    } 
 
    public void drink(){
        System.out.println("Snake dived into water and drink.");
    }
}

3.7 Farmer 類

Farmer類不屬於 Animal類族, 但是Farmer農夫可以給各種動物, 喂水.

Farmer類有2個關鍵方法, 分別是

bringWater(String destination)

把水帶到某個地點

另1個就是feedWater了,

feedWater這個方法分為三步:

  • 首先是農夫帶水到飼養室,(bringWater())

  • 接着被喂水動物走到飼養室,(move())

  • 接着動物喝水(drink())

Farmer可以給老虎喂水, 可以給山羊喂水, 還可以給蛇喂水, 那么feedWater()里的參數類型到底是老虎,山羊還是蛇呢.

實際上因為老虎,山羊, 蛇都繼承自Animal這個類, 所以feedWater里的參數類型設為Animal就可以了.

Farmer類首先叼用bringWater("飼養室"),至於這個動物是如何走到飼養室和如何喝水的, Farmer類則不用關心.

因為執行時, Animal超類會根據引用指向的對象類型不同 而 指向不同的被重寫的方法.  這個就是多態的意義.

代碼如下:

class Farmer{
    public void bringWater(String destination){
        System.out.println("Farmer bring water to " + destination + ".");
    }
 
    public void feedWater(Animal a){ // polymorphism
        this.bringWater("Feeding Room");
        a.move("Feeding Room");
        a.drink();
    }
 
}

3.8 執行農夫喂水的代碼.

下面的代碼是1個農夫依次喂水給一只老虎, 一只羊, 以及一條蛇

 public static void f(){
        Farmer fm = new Farmer();
        Snake sn = new Snake();
        Goat gt = new Goat();
        Tiger tg = new Tiger();
 
        fm.feedWater(sn);
        fm.feedWater(gt);
        fm.feedWater(tg);
    }

農夫只負責帶水過去制定地點, 而不必關心老虎, 蛇, 山羊它們是如何過來的. 它們如何喝水. 這些農夫都不必關心.

只需要調用同1個方法feedWater.

執行結果:

     [java] Farmer bring water to Feeding Room.
     [java] Snake crawled to Feeding Room.
     [java] Snake dived into water and drink.
     [java] Farmer bring water to Feeding Room.
     [java] Goat moved to Feeding Room.
     [java] Goat lower it's head and drink.
     [java] Farmer bring water to Feeding Room.
     [java] Goat moved to Feeding Room.
     [java] Goat lower it's head and drink.

不使用多態的后果?:

而如果老虎, 蛇, 山羊的drink() 方法不是重寫自同1個抽象方法的話, 多態就不能實現. 農夫類就可能要根據參數類型的不同而重載很多個  feedWater()方法了.

而且每增加1個類(例如 獅子Lion)

就需要在農夫類里增加1個feedWater的重載方法 feedWater(Lion l)...

而接口跟抽象類類似,

這個就回答了不本文第一個問題.

1.為什么不直接在類里面寫對應的方法,  而要多寫1個接口(或抽象類)?

四. 抽象類解決不了的問題.

既然抽象類很好地實現了多態性, 那么什么情況下用接口會更加好呢?

對於上面的例子, 我們加一點需求.

Farmer 農夫多了1個技能, 就是給另1個動物喂兔子(囧).

  • BringAnimal(Animal a, String destination)     把兔子帶到某個地點...

  • feedAnimal(Animal ht, Animal a)            把動物a丟給動物ht

注意農夫並沒有把兔子宰了, 而是把小動物(a)丟給另1個被喂食的動物(ht).

那么問題來了, 那個動物必須有捕獵這個技能.  也就是我們要給被喂食的動物加上1個方法(捕獵) hunt(Animal a).

但是現實上不是所有動物都有捕獵這個技能的, 所以我們不應該把hunt(Animal a)方法加在Goat類和Rabbit類里,  只加在Tiger類和Snake類里.

而且老虎跟蛇的捕獵方法也不一樣, 則表明hunt()的方法體在Tiger類里和Snake類里是不一樣的.

下面有3個方案.

  1. 分別在Tiger類里和Snake類里加上Hunt() 方法.  其它類(例如Goat) 不加.

  2. 在基類Animal里加上Hunt()抽象方法. 在Tiger里和Snake里重寫這個Hunt() 方法.

  3. 添加肉食性動物這個抽象類.

先來說第1種方案.

這種情況下, Tiger里的Hunt(Animal a)方法與 Snake里的Hunt(Animal a)方法毫無關聯. 也就是說不能利用多態性.

導致Farm類里的feedAnimal()方法需要分別為Tiger 與 Snake類重載. 否決.

第2種方案:

如果在抽象類Animal里加上Hunt()方法, 則所有它的非抽象派生類都要重寫實現這個方法, 包括 Goat類和 Rabbit類.

這是不合理的, 因為Goat類根本沒必要用到Hunt()方法, 造成了資源(內存)浪費.

第3種方案:

加入我們在哺乳類動物下做個分叉, 加上肉食性哺乳類動物, 非肉食性哺乳動物這兩個抽象類?

首先,肉食性這種分叉並不准確, 例如很多腐蝕性動物不會捕獵, 但是它們是肉食性.

其次,這種方案會另類族圖越來越復雜, 假如以后再需要辨別能否飛的動物呢, 增加飛翔 fly()這個方法呢? 是不是還要分叉?

再次,很現實的問題, 在項目中, 你很可能沒機會修改上層的類代碼, 因為它們是用Jar包發布的, 或者你沒有修改權限.

這種情況下就需要用到接口了.

Java知音公眾號內回復“后端面試”,送你一份Java面試題寶典

五.接口與多態 以及 多繼承性.

上面的問題, 抽象類解決不了, 根本問題是Java的類不能多繼承.

因為Tiger類繼承了動物Animal類的特性(例如 move() 和 drink()) , 但是嚴格上來將 捕獵(hunt())並不算是動物的特性之一. 有些植物, 單細胞生物也會捕獵的.

所以Tiger要從別的地方來繼承Hunt()這個方法.  接口就發揮作用了.

5.1 Huntable接口

我們增加了1個Huntable接口.

接口里有1個方法hunt(Animal a), 就是捕捉動物, 至於怎樣捕捉則由實現接口的類自己決定.

代碼:

interface Huntable{
    public void hunt(Animal a);
}

5.2 Tiger 類

既然定義了1個Huntable(可以捕獵的)接口.

Tiger類就要實現這個接口並重寫接口里hunt()方法.

class Tiger extends Mammal implements Huntable{
    private static String name = "Tiger";
    public String getName(){
        return this.name;
    }
 
    public void move(String destination){
        System.out.println("Goat moved to " + destination + ".");
    }
 
    public void drink(){
        System.out.println("Goat lower it's head and drink.");
    }
 
    public void hunt(Animal a){
        System.out.println("Tiger catched " + a.getName() + " and eated it");
    }
 
}

5.3 Snake類

同樣:

class Snake extends Reptile implements Huntable{
    private static String name = "Snake";
    public String getName(){
        return this.name;
    }
 
    public void move(String destination){
        System.out.println("Snake crawled to " + destination + ".");
    } 
 
    public void drink(){
        System.out.println("Snake dived into water and drink.");
    }
 
    public void hunt(Animal a){
        System.out.println("Snake coiled " + a.getName() + " and eated it");
    }
}

可見同樣實現接口的hunt()方法, 但是蛇與老虎的捕獵方法是有區別的.

5.4 Farmer類

這樣的話. Farmer類里的feedAnimal(Animal ht, Animal a)就可以實現多態了.

class Farmer{
    public void bringWater(String destination){
        System.out.println("Farmer bring water to " + destination + ".");
    }
    
    public void bringAnimal(Animal a,String destination){
        System.out.println("Farmer bring " + a.getName() + " to " + destination + ".");
    }
 
    public void feedWater(Animal a){
        this.bringWater("Feeding Room");
        a.move("Feeding Room");
        a.drink();
    }
 
    public void feedAnimal(Animal ht , Animal a){
        this.bringAnimal(a,"Feeding Room");
        ht.move("Feeding Room");
        Huntable hab = (Huntable)ht;
        hab.hunt(a);
    }
 
}

關鍵是這一句

Huntable hab = (Huntable)ht;

本文一開始講過了, 接口的引用可以指向實現該接口的對象.

當然, 如果把Goat對象傳入Farmer的feedAnimal()里就會有異常, 因為Goat類沒有實現該接口. 上面那個代碼執行失敗.

如果要避免上面的問題.

可以修改feedAnimal方法:

    public void feedAnimal(Huntable hab, Animal a){
        this.bringAnimal(a,"Feeding Room");
        Animal ht = (Animal)hab;
        ht.move("Feeding Room");
        hab.hunt(a);
    }

這樣的話, 傳入的對象就必須是實現了Huntable的對象, 如果把Goat放入就回編譯報錯.

但是里面一樣有一句強制轉換

Animal ht = (Animal)hab

反而更加不安全, 因為實現的Huntable的接口的類不一定都是Animal的派生類. 相反, 接口的出現就是鼓勵多種不同的類實現同樣的功能(方法)

例如,假如一個機械類也可以實現這個接口, 那么那個機械就可以幫忙打獵了(囧)

1個植物類(例如捕蠅草),實現這個接口, 也可以捕獵蒼蠅了.

也就是說, 接口不會限制實現接口的類的類型.

執行輸出:

     [java] Farmer bring Rabbit to Feeding Room.
     [java] Snake crawled to Feeding Room.
     [java] Snake coiled Rabbit and eated it
     [java] Farmer bring Rabbit to Feeding Room.
     [java] Goat moved to Feeding Room.
     [java] Tiger catched Rabbit and eated it

這樣, Tiger類與Snake類不但繼承了Animal的方法, 還繼承(實現)了接口Huntable的方法, 一定程度上彌補java的class不支持多繼承的特點.

六.接口上應用泛型.

上面的Huntable里還是有點限制的,

就是它里面的hunt()方法的參數是 Animal a, 也就是說這個這個接口只能用於捕獵動物.

但是在java的世界里, 接口里的方法(行為)大多數是與類的類型無關的.

也就是說, Huntable接口里的hunt()方法里不單只可以捕獵動物, 還可以捕獵其他東西(例如 捕獵植物... 敵方機械等)

6.1 Huntable接口

首先要在Huntable接口上添加泛型標志:<T>

interface Huntable<T>{
    public void hunt(T o);
}

然后里面的hunt()的參數的類型就寫成T, 表示hunt()方法可以接受多種參數, 取決於實現接口的類.

6.2 Tiger類(和Snake類)

同樣, 定義tiger類時必須加上接口的泛型標志<Animal>, 表示要把接口應用在Animal這種類型.

class Tiger extends Mammal implements Huntable<Animal>{
    private static String name = "Tiger";
    public String getName(){
        return this.name;
    }
 
    public void move(String destination){
        System.out.println("Goat moved to " + destination + ".");
    }
 
    public void drink(){
        System.out.println("Goat lower it's head and drink.");
    }
 
    public void hunt(Animal a){
        System.out.println("Tiger catched " + a.getName() + " and eated it");
    }
 
}

這樣, 在里面hunt()參數就可以指明類型Animal了,  表示老虎雖然有捕獵這個行為, 但是只能捕獵動物.Java知音公眾號內回復“后端面試”,送你一份Java面試題寶典

七.什么情況下應該使用接口而不用抽象類.

好了, 回到本文最重要的一個問題.

做個總結

  1. 需要實現多態

  2. 要實現的方法(功能)不是當前類族的必要(屬性).

  3. 要為不同類族的多個類實現同樣的方法(功能).

下面是分析:

7.1 需要實現多態

很明顯, 接口其中一個存在意義就是為了實現多態. 這里不多說了.

而抽象類(繼承) 也可以實現多態

7.2. 要實現的方法(功能)不是當前類族的必要(屬性).

上面的例子就表明, 捕獵這個方法不是動物這個類必須的,在動物的派生類中, 有些類需要, 有些不需要.

如果把捕獵方法卸載動物超類里面是不合理的浪費資源.

所以把捕獵這個方法封裝成1個接口, 讓派生類自己去選擇實現!

7.3. 要為不同類族的多個類實現同樣的方法(功能).

上面說過了, 其實不是只有Animal類的派生類才可以實現Huntable接口.

如果Farmer實現了這個接口, 那么農夫自己就可以去捕獵動物了...

我們拿另個常用的接口Comparable來做例子.

這個接口是應用了泛型,

首先, 比較(CompareTo) 這種行為很難界定適用的類族, 實際上, 幾乎所有的類都可以比較.

比如 數字類可以比較大小,   人類可以比較財富,  動物可以比較體重等.

所以各種類都可以實現這個比較接口.

一旦實現了這個比較接口. 就可以開啟另1個隱藏技能:

就是可以利用Arrays.sort()來進行排序了.

就如實現了捕獵的動物,

可以被農夫Farmer喂兔子一樣...

八.接口為什么會被叫做接口, 跟真正的接口例如usb接口有聯系嗎?

對啊, 為什么叫接口, 而不叫插件(plugin)呢,  貌似java接口的功能更類似1個插件啊.

插上某個插件, 就有某個功能啊.

實際上, 插件與接口是相輔相成的.

例如有1個外部存儲插件(U盤), 也需要使用設備具有usb接口才能使用啊.

再舉個具體的例子.

個人電腦是由大型機發展而來的

大型機->小型機->微機(PC)

而筆記本是繼承自微機的.

那么問題來了.

對於, 計算機的CPU/內存/主板/獨顯/光驅/打印機 有很多功能(方法/行為), 那么到底哪些東西是繼承, 哪些東西是接口呢.

首先,  cpu/內存/主板 是從大型機開始都必備的, 任何計算機都不能把它們去掉.

所以, 這三樣東西是繼承的, 也就說筆記本的cpu/內存/主板是繼承自微機(PC)的

但是/光驅/呢,    現實上很多超薄筆記本不需要光驅的功能.

如果光驅做成繼承, 那么筆記本就必須具有光驅, 然后屏蔽光驅功能, 那么這台筆記本還能做到超薄嗎? 浪費了資源.

所以光驅,打印機這些東西就應該做成插件.

然后, 在筆記本上做1個可以插光驅和打印機的接口(usb接口).

也就是說, PC的派生類, 有些(筆記本)可以不實現這個接口, 有些(台式機)可以實現這個接口,只需要把光驅插到這個接口上.

至於光驅是如何實現的,

例如一些pc派生類選擇實現藍光光驅, 有些選擇刻錄機.  但是usb接口本身並不關心. 取決與實現接口的類.

這個就是現實意義上的多態性啊.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM