介面和抽象理解

hetaoo發表於2019-02-13

一、抽象類:

我們都知道在物件導向的領域一切都是物件,同時所有的物件都是通過類來描述的,但是並不是所有的類都是來描述物件的。如果一個類沒有足夠的資訊來描述一個具體的物件,而需要其他具體的類來支撐它,那麼這樣的類我們稱它為抽象類。比如new Animal(),我們都知道這個是產生一個動物Animal物件,但是這個Animal具體長成什麼樣子我們並不知道,它沒有一個具體動物的概念,所以他就是一個抽象類,需要一個具體的動物,如狗、貓來對它進行特定的描述,我們才知道它長成啥樣。

在物件導向領域由於抽象的概念在問題領域沒有對應的具體概念,所以用以表徵抽象概念的抽象類是不能例項化的。

同時,抽象類體現了資料抽象的思想,是實現多型的一種機制。它定義了一組抽象的方法,至於這組抽象方法的具體表現形式有派生類來實現。同時抽象類提供了繼承的概念,它的出發點就是為了繼承,否則它沒有存在的任何意義。所以說定義的抽象類一定是用來繼承的,同時在一個以抽象類為節點的繼承關係等級鏈中,葉子節點一定是具體的實現類。

在使用抽象類時需要注意幾點:

  • 抽象類不能被例項化,例項化的工作應該交由它的子類來完成,它只需要有一個引用即可。
  • 抽象方法必須由子類來進行重寫。
  • 只要包含一個抽象方法的抽象類,該方法必須要定義成抽象類,不管是否還包含有其他方法。
  • 抽象類中可以包含具體的方法,當然也可以不包含抽象方法。
  • abstract不能與final並列修飾同一個類。
  • abstract 不能與private、static、final或native並列修飾同一個方法。

例項:

定義一個抽象動物類Animal,提供抽象方法叫cry(),貓、狗都是動物類的子類,由於cry()為抽象方法,所以Cat、Dog必須要實現cry()方法。如下:

public abstract class Animal {
    public abstract void cry();
}

public class Cat extends Animal{

    @Override
    public void cry() {
        System.out.println("貓叫:喵喵...");
    }
}

public class Dog extends Animal{

    @Override
    public void cry() {
        System.out.println("狗叫:汪汪...");
    }

}

public class Test {

    public static void main(String[] args) {
        Animal a1 = new Cat();
        Animal a2 = new Dog();

        a1.cry();
        a2.cry();
    }
}

--------------------------------------------------------------------
Output:
貓叫:喵喵...
狗叫:汪汪...複製程式碼

建立抽象類和抽象方法非常有用,因為他們可以使類的抽象性明確起來,並告訴使用者和編譯器打算怎樣使用他們.抽象類還是有用的重構器,因為它們使我們可以很容易地將公共方法沿著繼承層次結構向上移動。

二、介面

介面是一種比抽象類更加抽象的“類”。這裡給“類”加引號是我找不到更好的詞來表示,但是我們要明確一點就是,介面本身就不是類,從我們不能例項化一個介面就可以看出。如new Runnable();肯定是錯誤的,我們只能new它的實現類。

介面是用來建立類與類之間的協議,它所提供的只是一種形式,而沒有具體的實現。同時實現該介面的實現類必須要實現該介面的所有方法,通過使用implements關鍵字,他表示該類在遵循某個或某組特定的介面,同時也表示著“interface只是它的外貌,但是現在需要宣告它是如何工作的”。

介面是抽象類的延伸,java了保證資料安全是不能多重繼承的,也就是說繼承只能存在一個父類,但是介面不同,一個類可以同時實現多個介面,不管這些介面之間有沒有關係,所以介面彌補了抽象類不能多重繼承的缺陷,但是推薦繼承和介面共同使用,因為這樣既可以保證資料安全性又可以實現多重繼承。

在使用介面過程中需要注意如下幾個問題:

  • 一個Interface的方所有法訪問許可權自動被宣告為public。確切的說只能為public,當然你可以顯示的宣告為protected、private,但是編譯會出錯!
  • 介面中可以定義“成員變數”,或者說是不可變的常量,因為介面中的“成員變數”會自動變為為public static final。可以通過類命名直接訪問:ImplementClass.name。
  • 介面中不存在實現的方法。
  • 實現介面的非抽象類必須要實現該介面的所有方法。抽象類可以不用實現。
  • 不能使用new操作符例項化一個介面,但可以宣告一個介面變數,該變數必須引用(refer to)一個實現該介面的類的物件。可以使用 instanceof 檢查一個物件是否實現了某個特定的介面。例如:if(anObject instanceof Comparable){}。
  • 在實現多介面的時候一定要避免方法名的重複。

三、抽象類與介面的區別

儘管抽象類和介面之間存在較大的相同點,甚至有時候還可以互換,但這樣並不能彌補他們之間的差異之處。下面將從語法層次和設計層次兩個方面對抽象類和介面進行闡述。

3.1 語法層次

在語法層次,java語言對於抽象類和介面分別給出了不同的定義。下面已Demo類來說明他們之間的不同之處。

使用抽象類來實現:

public abstract class Demo {
    abstract void method1();

    void method2(){
        //實現
    }
}複製程式碼

使用介面來實現

interface Demo {
    void method1();
    void method2();
}複製程式碼

抽象類方式中,抽象類可以擁有任意範圍的成員資料,同時也可以擁有自己的非抽象方法,但是介面方式中,它僅能夠有靜態、不能修改的成員資料(但是我們一般是不會在介面中使用成員資料),同時它所有的方法都必須是抽象的。在某種程度上來說,介面是抽象類的特殊化。

對子類而言,它只能繼承一個抽象類(這是java為了資料安全而考慮的),但是卻可以實現多個介面。

3.2 設計層次

上面只是從語法層次和程式設計角度來區分它們之間的關係,這些都是低層次的,要真正使用好抽象類和介面,我們就必須要從較高層次來區分了。只有從設計理念的角度才能看出它們的本質所在。一般來說他們存在如下三個不同點:

  • 抽象層次不同。抽象類是對類抽象,而介面是對行為的抽象。抽象類是對整個類整體進行抽象,包括屬性、行為,但是介面卻是對類區域性(行為)進行抽象。
  • 跨域不同。抽象類所跨域的是具有相似特點的類,而介面卻可以跨域不同的類。我們知道抽象類是從子類中發現公共部分,然後泛化成抽象類,子類繼承該父類即可,但是介面不同。實現它的子類可以不存在任何關係,共同之處。例如貓、狗可以抽象成一個動物類抽象類,具備叫的方法。鳥、飛機可以實現飛Fly介面,具備飛的行為,這裡我們總不能將鳥、飛機共用一個父類吧!所以說抽象類所體現的是一種繼承關係,要想使得繼承關係合理,父類和派生類之間必須存在”is-a” 關係,即父類和派生類在概念本質上應該是相同的。對於介面則不然,並不要求介面的實現者和介面定義在概念本質上是一致的, 僅僅是實現了介面定義的契約而已。
  • 設計層次不同。對於抽象類而言,它是自下而上來設計的,我們要先知道子類才能抽象出父類,而介面則不同,它根本就不需要知道子類的存在,只需要定義一個規則即可,至於什麼子類、什麼時候怎麼實現它一概不知。比如我們只有一個貓類在這裡,如果你這是就抽象成一個動物類,是不是設計有點兒過度?我們起碼要有兩個動物類,貓、狗在這裡,我們在抽象他們的共同點形成動物抽象類吧!所以說抽象類往往都是通過重構而來的!但是介面就不同,比如說飛,我們根本就不知道會有什麼東西來實現這個飛介面,怎麼實現也不得而知,我們要做的就是事前定義好飛的行為介面。所以說抽象類是自底向上抽象而來的,介面是自頂向下設計出來的。 

為了更好的闡述他們之間的區別,下面將使用一個例子來說明。該例子引自:blog.csdn.net/ttgjz/artic…

我們有一個Door的抽象概念,它具備兩個行為open()和close(),此時我們可以定義通過抽象類和介面來定義這個抽象概念:

抽象類:

 abstract class Door{
    abstract void open();
    abstract void close();
}複製程式碼

介面

 interface Door{
    void open();
    void close();
}複製程式碼

至於其他的具體類可以通過使用extends使用抽象類方式定義Door或者Implements使用介面方式定義Door,這裡發現兩者並沒有什麼很大的差異。

但是現在如果我們需要門具有報警的功能,那麼該如何實現呢?

解決方案一:給Door增加一個報警方法:clarm();

 abstract class Door{
    abstract void open();
    abstract void close();
    abstract void alarm();
}複製程式碼

或者

 interface Door{
    void open();
    void close();
    void alarm();
}複製程式碼

這種方法違反了物件導向設計中的一個核心原則 ISP (Interface Segregation Principle)—見批註,在Door的定義中把Door概念本身固有的行為方法和另外一個概念”報警器”的行為方 法混在了一起。這樣引起的一個問題是那些僅僅依賴於Door這個概念的模組會因為”報警器”這個概念的改變而改變,反之依然。

解決方案二

既然open()、close()和alarm()屬於兩個不同的概念,那麼我們依據ISP原則將它們分開定義在兩個代表兩個不同概念的抽象類裡面,定義的方式有三種:

  • 1、兩個都使用抽象類來定義。
  • 2、兩個都使用介面來定義。
  • 3、一個使用抽象類定義,一個是用介面定義。

由於java不支援多繼承所以第一種是不可行的。後面兩種都是可行的,但是選擇何種就反映了你對問題域本質的理解。

如果選擇第二種都是介面來定義,那麼就反映了兩個問題:1、我們可能沒有理解清楚問題域,AlarmDoor在概念本質上到底是門還報警器。2、如果我們對問題域的理解沒有問題,比如我們在分析時確定了AlarmDoor在本質上概念是一致的,那麼我們在設計時就沒有正確的反映出我們的設計意圖。因為你使用了兩個介面來進行定義,他們概念的定義並不能夠反映上述含義。

第三種,如果我們對問題域的理解是這樣的:AlarmDoor本質上Door,但同時它也擁有報警的行為功能,這個時候我們使用第三種方案恰好可以闡述我們的設計意圖。AlarmDoor本質是們,所以對於這個概念我們使用抽象類來定義,同時AlarmDoor具備報警功能,說明它能夠完成報警概念中定義的行為功能,所以alarm可以使用介面來進行定義。如下:

abstract class Door{
    abstract void open();
    abstract void close();
}

interface Alarm{
    void alarm();
}

class AlarmDoor extends Door implements Alarm{
    void open(){}
    void close(){}
    void alarm(){}
}複製程式碼

這種實現方式基本上能夠明確的反映出我們對於問題領域的理解,正確的揭示我們的設計意圖。其實抽象類表示的是”is-a”關係,介面表示的是”like-a”關係,大家在選擇時可以作為一個依據,當然這是建立在對問題領域的理解上的,比如:如果我們認為AlarmDoor在概念本質上是報警器,同時又具有Door的功能,那麼上述的定義方式就要反過來了。

批註:

ISP(Interface Segregation Principle):物件導向的一個核心原則。它表明使用多個專門的介面比使用單一的總介面要好。

一個類對另外一個類的依賴性應當是建立在最小的介面上的。

一個介面代表一個角色,不應當將不同的角色都交給一個介面。沒有關係的介面合併在一起,形成一個臃腫的大介面,這是對角色和介面的汙染。

四、總結

  • 1、抽象類在java語言中所表示的是一種繼承關係,一個子類只能存在一個父類,但是可以存在多個介面。
  • 2、 在抽象類中可以擁有自己的成員變數和非抽象類方法,但是介面中只能存在靜態的不可變的成員資料(不過一般都不在介面中定義成員資料),而且它的所有方法都是抽象的。
    3、抽象類和介面所反映的設計理念是不同的,抽象類所代表的是“is-a”的關係,而介面所代表的是“like-a”的關係。

抽象類和介面是java語言中兩種不同的抽象概念,他們的存在對多型提供了非常好的支援,雖然他們之間存在很大的相似性。但是對於他們的選擇往往反應了您對問題域的理解。只有對問題域的本質有良好的理解,才能做出正確、合理的設計。


相關文章