菜鳥成長系列-多型、介面和抽象類

glmapper發表於2017-11-17

物件導向的三大特性:封裝、繼承、多型。從一定角度來看,封裝和繼承幾乎都是為多型而準備的。這是我們最後一個概念,也是最重要的知識點。

多型的定義:指允許不同類的物件對同一訊息做出響應。即同一訊息可以根據傳送物件的不同而採用多種不同的行為方式。(傳送訊息就是函式呼叫)

動態繫結

  • 靜態繫結和動態繫結
    這裡所謂的繫結,即一個方法的呼叫與方法所在的類(方法主體)關聯起來。

    靜態繫結(前期繫結):即在程式執行前,即編譯的時候已經實現了該方法與所在類的繫結,像C就是靜態繫結。
    java中只有static,final,private和構造方法是靜態繫結,其他的都屬於動態繫結,而private的方法其實也是final方法(隱式),而構造 方法其實是一個static方法(隱式),所以可以看出把方法宣告為final,第一可以讓他不被重寫,第二也可以關閉它的動態繫結。

    動態繫結(後期繫結):執行時根據物件的型別進行繫結,java中的大多數方法都是屬於動態繫結,也就是實現多型的基礎。
    java實現了後期繫結,則必須提供一些機制,可在執行期間判斷物件的型別,並分別呼叫適當的方法。 也就是說,編譯的時候該方法不與所在類繫結,編譯器此時依然不知道物件的型別,但方法呼叫機制能自己去調查,找到正確的方法主體。java裡實現動態繫結的是JVM.

動態繫結是實現多型的技術,是指在執行期間判斷所引用物件的實際型別,根據其實際的型別呼叫其相應的方法。

多型的作用

消除型別之間的耦合關係。即:把不同的子類物件都當作父類來看,可以遮蔽不同子類物件之間的差異,寫出通用的程式碼,做出通用的程式設計,以適應需求的不斷變化。

多型存在的三個必要條件

一、要有繼承;
二、要有重寫;
三、父類引用指向子類物件。

多型的優點

1.可替換性(substitutability)。多型對已存在程式碼具有可替換性。
2.可擴充性(extensibility)。多型對程式碼具有可擴充性。增加新的子類不影響已存在類的多型性、繼承性,以及其他特性的執行和操作。實際上新加子類更容易獲得多型功能。
3.介面性(interface-ability)。多型是超類通過方法簽名,向子類提供了一個共同介面,由子類來完善或者覆蓋它而實現的。
4.靈活性(flexibility)。它在應用中體現了靈活多樣的操作,提高了使用效率。
5.簡化性(simplicity)。多型簡化對應用軟體的程式碼編寫和修改過程,尤其在處理大量物件的運算和操作時,這個特點尤為突出和重要。

多型的實現方式

Java中多型的實現方式:

  • 介面實現
  • 繼承父類進行方法重寫
  • 同一個類中進行方法過載。

    例子

    無論工作還是學習中,筆都是我們經常用到的工具。但是筆的種類又非常的繁多,鉛筆、簽字筆、水筆、毛筆、鋼筆...。現在我們要對“筆”進行抽象,抽象成一個抽象父類“Pen”
package com.glmapper.demo.base;

/**
 * 抽象父類:筆
 * @author glmapper
 */
public abstract class Pen {
    //筆的長度
    private int length;
    //顏色
    private String color;
    //型別
    private String type;
    //價格
    private double price;

    //寫字
    public abstract void write(String cnt);

    public int getLength() {
        return length;
    }
    public void setLength(int length) {
        this.length = length;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }

}複製程式碼

現在有兩個子類,分別是:鉛筆和鋼筆。

鉛筆類,繼承父類Pen,並重寫write方法

package com.glmapper.demo.base;
/**
 * 鉛筆類 繼承父類 筆(滿足必要條件一:有繼承【其實如果是介面的話,implement實現也是可以的】)
 * @author glmapper
 *
 */
public class Pencil extends Pen{
    /**
     * 父類的抽象方法委託子類具體實現:覆蓋
     */
     //滿足必要條件二:要有重寫【當然,如果是對於write有過載也是可以的,不同的概念而已】
    @Override
    public void write(String cnt) {
        System.out.println("這是一隻鉛筆寫的內容,內容是:"+cnt);
    }

}複製程式碼
  • 鋼筆類,繼承父類Pen,並重寫write方法
package com.glmapper.demo.base;
/**
 * 鋼筆類 繼承父類 筆
 * @author 17070738
 *
 */
public class Fountainpen extends Pen{

    @Override
    public void write(String cnt) {
        System.out.println("這是一支鋼筆寫的內容,內容是:"+cnt);
    }

}複製程式碼

測試:

package com.glmapper.demo.base;

public class MainTest {

    public static void main(String[] args) {

    /*    Pen pen= new Pencil();*/
        //必要條件三:父類引用指向子類物件。
        Pen pen= new Fountainpen();
        pen.write("我是一支筆");

    }
}複製程式碼

輸出結果:這是一支鋼筆寫的內容,內容是:我是一支筆

說明

可替換性:多型對筆Pen類工作,對其他任何子類,如鉛筆、鋼筆,也同樣工作。
可擴充性:在實現了鉛筆、鋼筆的多型基礎上,很容易增添“筆”類的多型性。

介面

一個Java介面,就是一些方法特徵的集合。【本文角度並非是java基礎角度來說,主要是以設計模式中的應用為背景,因此對於相關定義及用法請自行學習。www.runoob.com/java/java-i…
我們在平時的工作中,提到介面,一般會含有兩種不同的含義,

  • 指的是java介面,這是一種java語言中存在的結構,有特定的語法和結構
  • 指一個類所具有的方法特徵的集合,是一種邏輯上的抽象。

前者叫做“java介面”,後者叫著“介面”。例如:java.lang.Runnable就是一個java介面。

為什麼使用介面

我們考慮一下,假如沒有介面會怎麼樣呢?一個類總歸是可以通過繼承來進行擴充套件的,這難道不足以我們的實際應用嗎?
一個物件需要知道其他的一些物件,並且與其他的物件發生相互的作用,這是因為這些物件需要借住於其他物件的行為以便於完成一項工作。這些關於其他物件的知識,以及對其他物件行為的呼叫,都是使用硬程式碼寫在類裡面的,可插入性幾乎為0。如:鋼筆中需要鋼筆水,鋼筆水有不同的顏色:
鋼筆水類:

package com.glmapper.demo.base;
/**
 * 鋼筆墨水
 * @author glmapper
 */
public class PenInk {
    //墨水顏色
    private String inkColor;

    public String getInkColor() {
        return inkColor;
    }

    public void setInkColor(String inkColor) {
        this.inkColor = inkColor;
    }

    public PenInk(String inkColor) {
        super();
        this.inkColor = inkColor;
    }

}複製程式碼

鋼筆中持有一個墨水類的物件引用:

package com.glmapper.demo.base;
/**
 * 鋼筆類 繼承父類 筆
 * @author 17070738
 *
 */
public class Fountainpen extends Pen{
    //引用持有
    PenInk ink =new PenInk("black");
    @Override
    public void write(String cnt) {
        System.out.println("鋼筆墨水顏色是:"+ink.getInkColor());
        System.out.println("這是一支鋼筆寫的內容,內容是:"+cnt);
    }
}複製程式碼

但是這種時候,我們需要換一種顏色怎麼辦呢?就必須要對Fountainpen中的程式碼進行修改,將建立PenInk物件時的inkColor屬性進行更改;現在假如我們有一個具體的類,提供某種使用硬程式碼寫在類中的行為;
現在,要提供一些類似的行為,並且可以實現動態的可插入,也就是說,要能夠動態的決定使用哪一種實現。一種方案就是為這個類提供一個抽象父類,且宣告出子類要提供的行為,然後讓這個具體類繼承自這個抽象父類。同時,為這個抽象父類提供另外一個具體的子類,這個子類以不同的方法實現了父類所宣告的行為。客戶端可以動態的決定使用哪一個具體的子類,這是否可以提供可插入性呢?
改進之後的程式碼:
子類1:黑色墨水

package com.glmapper.demo.base;
/**
 * 黑色墨水
 * @author glmapper
 */
public class BlackInk extends PenInk{

    public BlackInk() {
        super("black");
    }
}複製程式碼

子類2:藍色墨水

package com.glmapper.demo.base;
/**
 * 藍色墨水
 * @author glmapper
 */
public class BlueInk extends PenInk{

    public BlueInk() {
        super("blue");
    }
}複製程式碼

鋼筆類引用:

package com.glmapper.demo.base;
/**
 * 鋼筆類 繼承父類 筆
 * @author 17070738
 *
 */
public class Fountainpen extends Pen{
    PenInk ink ;
    //通過建構函式初始化PenInk ,PenInk由具體子類來實現
    public Fountainpen(PenInk ink) {
        this.ink = ink;
    }
    @Override
    public void write(String cnt) {
        System.out.println("鋼筆墨水顏色是:"+ink.getInkColor());
        System.out.println("這是一支鋼筆寫的內容,內容是:"+cnt);
    }
}複製程式碼

客戶端呼叫:

public static void main(String[] args) {
        /**
         * 使用黑色墨水子類
         */
        Pen pen= new Fountainpen(new BlackInk());
        pen.write("我是一支筆");

    }複製程式碼

從上面程式碼可以看出,確實可以在簡單的情況下提供了動態可插入性。

但是由於java語言是一個單繼承的語言,換言之,一個類只能有一個超類,因此,在很多情況下,這個具體類可能已經有了一個超類,這個時候,要給他加上一個新的超類是不可能的。如果硬要做的話,就只好把這個新的超類加到已有的超類上面,形成超超類的情況,如果這個超超類的位置也已經被佔用了,就只好繼續向上移動,直到移動到類等級結構的最頂端。這樣一來,對一個具體類的可插入性設計,就變成了對整個等級結構中所有類的修改。這種還是假設這些超類是我們可以控制的,如果某些超類是由一些軟體商提供的,我們無法修改,怎麼辦呢?因此,假設沒有介面,可插入性就沒有了保證。

型別

java介面(以及java抽象類)用來宣告一個新的型別。
java設計師應當主要使用java介面和抽象類而不是具體類進行變數的型別宣告、引數的型別宣告、方法的返還型別宣告,以及資料型別的轉換等。當然,一個更好的做法是僅僅使用java介面,而不要使用抽象java類來做到上面這些。在理想的情況下,一個具體java類應當只實現java介面和抽象類中宣告過的方法,而不應該給出多餘的方法。

  • 型別等級結構
    java介面(以及抽象類)一般用來作為一個型別的等級結構的起點
    java的型別是以型別等級結構的方式組織起來的,在一個型別等級結構裡面,一個型別可以有一系列的超型別,這時這個型別叫做其超型別的子型別。子型別的關係是傳遞性:型別甲是型別乙的子型別,型別乙是型別丙的子型別,那麼型別甲就是型別丙的子型別。
  • 混合型別
    如果一個類已經有一個主要的超型別,那麼通過實現一個介面,這個類可以擁有另一個次要的超型別。這種次要的超型別就叫做混合型別。例如:在java中,

TreeMap類有多個型別,它的主要型別是AbstractMap,這是一種java的聚集;而Cloneable介面則給出了一個次要型別,這個型別說明當前類的物件是可以被克隆;同時Serializable也是一個次要型別,它表明當前類的物件是可以被序列化的。而NavigableMap繼承了SortedMap,因為之前說到過,子型別是可以傳遞的,因此對於TreeMap來說,SortedMap(或者說NavigableMap)表明這個聚集類是可以排序的。

介面的一些用法

  • 單介面方法:介面中只有一個方法;java語言中有很多但方法介面的使用,Runnalble介面中的run()方法。
    public interface Runnable {
      /**
       * When an object implementing interface <code>Runnable</code> is used
       * to create a thread, starting the thread causes the object's
       * <code>run</code> method to be called in that separately executing
       * thread.
       * <p>
       * The general contract of the method <code>run</code> is that it may
       * take any action whatsoever.
       *
       * @see     java.lang.Thread#run()
       */
      public abstract void run();
    }複製程式碼
  • 標識介面:沒有任何方法和屬性的介面;標識介面不對實現它的類有任何語義上的要求,僅僅是表明實現該介面的類屬於一個特定的型別。上面說到的Serializable介面就是一種標識介面。
public interface Serializable {
}複製程式碼
  • 常量介面:用java介面來宣告一些常量
package com.glmapper.demo.base;

public interface MyConstants {
    public static final String USER_NAME="admin";
};複製程式碼

這樣一來,凡是實現這個介面的類都會自動繼承這些常量,並且都可以像使用自己的常量一樣,不需要再用MyConstants.USER_NAME來使用。

抽象類

在java語言裡面,類有兩種,一種是具體類,一種是抽象類。在上面給出的程式碼中,使用absract修飾的類為抽象類。沒有被abstract修飾的類是具體類。抽象類通常代表一個抽象概念,它提供一個繼承的出發點。而具體類則不同,具體類可以被例項化,應當給出一個有邏輯實現的物件模板。由於抽象類不可以被例項化,因此一個程式設計師設計一個新的抽象類,一定是用來被繼承的。(不建議使用具體類來進行相關的繼承)。

關於程式碼重構

假設有兩個具體類,類A和類B,類B是類A的子類,那麼一個比較簡單的方案應該是建立一個抽象類(或者java介面),暫定為C,然後讓類A和類B成為抽象類C的子類【沒有使用UML的方式來繪製,請見諒哈】。


上面其實就是里氏替換原則,後面會具體介紹到的。這種重構之後,我們需要做的就是如何處理類A和類B的共同程式碼和共同資料。下面給出相關準則。

  • 抽象類應當擁有儘可能多的共同程式碼


在一個繼承等級結構中,共同的程式碼應當儘量向結構的頂層移動,將重複的程式碼從子類中抽離,放在抽象父類中,提高程式碼的複用率。這樣做的另外一個好處是,在程式碼發生改變時,我們只需要修改一個地方【因為共同程式碼均在父類中】。

  • 抽象類應當擁有儘可能少的資料
    資料的移動方向是從抽象類到具體類,也就是從繼承等級的頂層到底層的移動。我們知道,一個物件的資料不論是否使用都會佔用資源,因此資料應當儘量放到具體類或者繼承等級結構的低端。

Has - A 與Is -A

當一個類是另外一個類的角色時【我 有一個 玩具】,這種關係就不應當使用繼承來描述了,這個將會在後面說到的“合成/聚合複用原則”來描述。
Has - A: 我有一隻筆(聚合)
Is - A:鋼筆是一種筆(繼承)

關於子類擴充套件父類的責任

子類應當擴充套件父類的職責,而不是置換掉或者覆蓋掉超類的職責。如果一個子類需要將繼承自父類的責任取消或者置換後才能使用的話,就很有可能說明這個子類根本不屬於當前父類的子類,存在設計上的缺陷。

最後,說明下,我們在平時的工作中會經常使用的工具類,再次特地申明一下,我們也儘可能少的去從工具類進行繼承擴充套件。

參考:

相關文章