天天看點

子類和父類對象在進行類型轉換時_Java面向對象程式設計三大特征 - 多态

多态是面向對象程式設計的三大特征之一,是面向對象思想的終極展現之一。在了解多态之前需要先掌握繼承、重寫、父類引用指向子類對象的相關概念

一、抽象類

在繼承中,我們已經了解了子父類的關系以及如何對子父類進行設計,如果已經存在多個實體類,再去定義父類其實是不斷的抽取公共重合部分的過程,如果有需要将會産生多重繼承關系。在抽取整理的過程中,除了屬性可以複用,有很多方法一樣也可以複用,假如以圖形舉例:矩形、圓形,都可以具有周長和面積兩個方法,但是計算的方式完全不同,矩形和圓形之間肯定不能構成子父類關系,那麼隻能是同時去繼承一個父類,那麼問題就來了,這兩個類都有什麼共同點?

除了都是圖形好像并沒有什麼共同點,矩形有兩組邊長,圓形是通過半徑來描述,如果非要往一起聯系的話。。。Wait a moment(靈光一閃中,請勿打擾)!!!難道說是都可以計算出周長和面積?細細想來,也是能說出一番道理的,但是這好抽象啊!

如果真的是這樣,也隻能有一個模糊的思路,既然描述圖形的屬性不能夠共用那就分别放在兩個子類中吧,那麼計算周長和面積的方法要怎麼搞?如果在父類中定義相應的方法,那參數清單怎麼寫?方法體怎麼填?這個坑好像有點大,接下來,我們就要華麗地将這個坑填平。

1. 抽象與抽象類

在上面的例子中,我們遇到了一個情況,有兩個在邏輯上看似相關的類,我們想要把他們聯系起來,因為這樣做可以提高效率,但是在實施的過程中發現這個共同點有點太過模糊,難以用代碼描述,甚至于還不如分開用來的友善,這時就要引出抽象的概念,對應的關鍵詞為:abstract。

  • abstract可以修飾方法,修飾後被稱為抽象方法
  • abstract可以修飾類,修飾後被稱為抽象類
  • abstract不能與static修飾符同時使用

那麼使用了abstract又能如何呢?這代表指定的方法和類很難表述,那麼。。。就不用表述了!對于矩形類(Rectangle)與圓形類(Circle)的父類:圖形類(Figure),我們隻能總結出他具有計算周長和面積的方法,而具體的實作方法我們無法給出,隻有明确了圖形之後,才能給出具體的實作,于是我們使用抽象來描述這兩個方法, 被abstract修飾的方法不需要有方法體,且不能為private ,由于抽象方法沒有方法體,那麼如果被代碼調用到了怎麼辦呢?以下兩個限制規則可以杜絕這個問題:

  • 抽象方法隻能存在于抽象類中(接口在另外的文章中讨論)
  • 抽象類無法被直接執行個體化(匿名内部類的用法暫不做讨論)

既然抽象類不能被執行個體化,那麼自然也就不會調用到沒有方法體的那些方法了,那這些方法該怎麼被調用呢?我們需要一步一步的來梳理,至少目前我們已經能夠清晰的得到如下的關系圖了:

子類和父類對象在進行類型轉換時_Java面向對象程式設計三大特征 - 多态

2. 抽象類的特點

抽象類的本質依然是一個類(class),是以具備着一個普通類的所有功能,包括構造方法等的定義,總結一下,抽象類具有以下的幾個特點:

  • 抽象類由abstract修飾
  • 抽象類中允許出現抽象方法
  • 抽象類不能通過構造器直接執行個體化
  • 可以在抽象類中定義普通方法供子類繼承

現在,我們已經可以将抽象父類用代碼描述出來:

// 定義抽象類:圖形類public abstract class Figure{    // 定義計算周長的抽象方法:getC()    public abstract double getC();    // 定義計算面積的抽象方法:getS()    public abstract double getS();    // 定義描述圖形的非抽象方法:print()    public void print(){        System.out.println("這是一個圖形");    }}
           

3. 天生的父類:抽象類

現在我們已經有了一個抽象類,其中也定義了抽象方法,抽象類不能被直接執行個體化保證了抽象方法不會被直接調用到。回憶一下我們的出發點,費勁巴力的弄出個抽象類就是為了提取出兩個類比較抽象的共同點,那麼下一步自然是繼承了。

  • 抽象類不能直接執行個體化,是天生的抽象類
  • 如果一個類繼承了抽象類,那麼必須重寫父類中的抽象方法
  • 如果抽象類中定義了構造方法,可以被子類調用或在執行個體化子類對象時執行
  • 如果抽象類的子類依然是抽象類,可以不重寫抽象方法,将重寫操作留給下一級子類

二、重寫

重寫指的是子父類之間方法構成的關系,當子類繼承父類時,父類中可能已經存在了某些方法,那麼子類執行個體就可以直接進行調用。在有些時候由于子父類之間的差異,對于已經存在的方法想要做一些修改,這個時候我們可以利用重寫, 在子類中定義一個與父類中的方法完全相同的方法,包括傳回值類型和方法簽名(方法名 + 參數清單) ,此時就會構成重寫。這樣,子類執行個體在調用方法時就可以覆寫父類中的方法,具體的過程在後半部分闡述。

1. 重寫與重載的差別

我們在剛開始接觸方法的時候了解到了一個概念:重載,與重寫有些類似,容易混淆,如果知識點已經模糊可以進傳送門:Java程式的方法設計。總結一下,重寫和重載有以下差別:

  • 重載是同一個類中方法與方法之間的關系
  • 重寫是子父類間(接口與實作類間)方法與方法之間的關系
  • 構成重載:方法名相同,參數清單不同,傳回值類型可以不同
  • 構成重寫:方法名相同,參數清單相同,傳回值類型相同或為對應類型的子類
  • 構成重載的方法之間權限修飾符可以不同
  • 重寫方法的權限修飾符一定要大于被重寫方法的權限修飾符

有關于權限修飾符的作用如果不明确可以進傳送門: Java面向對象程式設計三大特征 - 封裝 。明确了重寫的含義之後,我們終于可以再度提筆,完成我們之前的例子:

// 定義矩形類public class Rectangle extends Figure{    // 定義構造器    public Rectangle(double height, double width) {        this.height = height;        this.width = width;    }    // 定義長和寬    public double height;    public double width;    // 重寫計算周長方法    @Override    public double getC() {        return 2 * (this.height + this.width);    }    // 重寫計算面積方法    @Override    public double getS() {        return this.height + this.width;    }    // 可選覆寫    @Override    public void print(){        System.out.println("矩形");    }}
           
// 定義圓形類public class Circle extends Figure{    // 定義構造器    public Circle(double radius) {        this.radius = radius;    }    // 定義半徑    public double radius;    // 重寫計算周長方法    @Override    public double getC() {        return 2 * Math.PI * this.radius;    }    // 重寫計算面積方法    @Override    public double getS() {        return Math.PI * Math.pow(this.radius, 2);    }    // 可選覆寫    @Override    public void print(){        System.out.println("圓形");    }}
           

2. 方法重寫的規則

  • 重寫的辨別為@Override
  • 方法的重寫發生在子類或者接口的實作類中
  • 被final聲明的方法不能被重寫
  • 被static聲明的方法不能被重寫,隻能聲明同結構的靜态方法,但是此時不構成重寫
  • 受限于權限修飾符,子類可能隻能重寫部分父類中的方法

3. 父類方法的顯式調用

從上面的代碼中可以看到,子類繼承父類後,如果存在抽象方法則比如重寫,由于父類中的方法是抽象的,是以無法調用。對于普通的方法,可以選擇性的重寫,一旦重寫我們可以認為父類的方法被覆寫了,其實這樣的形容是不準确的,在初學階段可以認為是覆寫。

比較規範的說法是:通過子類執行個體無法直接調用到父類中的同名方法了,但是在記憶體中依然存在着父類方法的結構,隻不過通路不到而已。另外,我們同樣可以在子類中顯式的調用出父類方法,這要用到super關鍵字。

  • super指代父類對象
  • super可以調用可通路的父類成員變量
  • super可以調用可通路的父類成員方法
  • super可以調用可通路的父類構造方法
  • 不能使用super調用父類中的抽象方法
  • 可以使用super調用父類中的靜态方法

如果我們需要在子類中調用父類方法或構造器,可以将代碼修改如下:

// 定義抽象類:圖形類public abstract class Figure{    // 在抽象類中定義構造器,在子類執行個體建立時執行    public Figure(){        System.out.println("Figure init");    }    // 定義計算周長的抽象方法:getC()    public abstract double getC();    // 定義計算面積的抽象方法:getS()    public abstract double getS();    // 定義描述圖形的非抽象方法:print()    public void print(){        System.out.println("這是一個圖形");    }}
           
// 定義矩形類public class Rectangle extends Figure{    // 定義構造器    public Rectangle(double height, double width) {        super();// 會調用預設的無參構造,代碼可省略        this.height = height;        this.width = width;    }    // 定義長和寬    public double height;    public double width;    // 重寫計算周長方法    @Override    public double getC() {        return 2 * (this.height + this.width);    }    // 重寫計算面積方法    @Override    public double getS() {        return this.height + this.width;    }    // 可選覆寫    @Override    public void print(){        super.print();// 調用父類方法        System.out.println("矩形");    }}
           
// 定義圓形類public class Circle extends Figure{    // 定義構造器    public Circle(double radius) {        super();// 會調用預設的無參構造,代碼可省略        this.radius = radius;    }    // 定義半徑    public double radius;    // 重寫計算周長方法    @Override    public double getC() {        return 2 * Math.PI * this.radius;    }    // 重寫計算面積方法    @Override    public double getS() {        return Math.PI * Math.pow(this.radius, 2);    }    // 可選覆寫    @Override    public void print(){        super.print();// 調用父類方法        System.out.println("圓形");    }}
           

三、父類引用指向子類對象

前面提到的概念消化完畢後,我們看一下子父類對象執行個體化的形式以及方法的執行效果。

1. 父類引用指向父類對象

如果父類是一個抽象類,則在等号右側不能直接使用new加構造方法的方式執行個體化,如果一定要得到父類執行個體,就要使用匿名内部類的用法,這裡不做讨論。

如果父類是一個普通類,那麼我們在初始化時,等号左側為父類型引用,等号右側為父類型對象(執行個體),這個時候其實和我們去建立一個類的對象并沒有什麼分别,不需要想着他是某某類的父類,因為 此時他不會和任何子類産生關系,隻是一個預設繼承了Object類的普通類 ,正常使用就好,能調用出的内容也都是父類中已定義的。

2. 子類引用指向子類對象

在進行子類執行個體化時,由于在子類的定義中繼承了父類,是以在建立子類對象時,會先一步建立父類對象。在進行調用時,根據權限修飾符,可以調用出子類及父類中可通路的屬性和方法。

public class Test{    public static void main(String[] args){        Rectangle rectangle = new Rectangle(5,10);        // 調用Rectangle中定義的方法,以子類重寫為準        rectangle.print();        System.out.println(rectangle.getC());// 得到矩形周長        System.out.println(rectangle.getS());// 得到矩形面積        Circle circle = new Circle(5);        // 調用Circle中定義的方法,以子類重寫為準        circle.print();        System.out.println(circle.getC());// 得到圓形周長        System.out.println(circle.getS());// 得到圓形面積    }}
           

3. 引用與對象之間的關系

在剛開始學習程式設計時,我們接觸了基本資料類型,可以直接用關鍵字聲明,定義變量指派後使用,并不需要使用new關鍵字。對于引用與對象的關系可以先參考之前的文章回顧一下: Java中的基本操作單元 - 類和對象 。在這裡我們重點要說明的是:等号左側的引用部分,與等号右側的部分在程式運作層面有怎樣的關聯。

與基本資料類型不同,在類中可以定義各種屬性和方法,使用時也需要先建立對象。等号左側的部分依然是一個類型的聲明,未指派時雖然預設情況下是null,但在程式編譯運作時,也會在棧中進行存儲,記錄了相應的結構資訊,他所指向的對象必須是一個和它 相容 的類型。

類的聲明引用存放在棧中,執行個體化得到的對象存放在堆中。

  • 在代碼編寫階段,能夠調用出的内容以等号左側類型為準
  • 在程式運作階段,具體的的執行效果以等号右側執行個體為準

下圖為引用與執行個體在記憶體中的關系示意圖,有關于Java對象在記憶體中的分布将在另外的文章中說明:

子類和父類對象在進行類型轉換時_Java面向對象程式設計三大特征 - 多态

4. 父類引用指向子類對象

了解了引用與對象的關系之後,就有了一個疑問,如果等号左側的聲明類型與等号右側的執行個體類型不一緻會怎麼樣呢?如果我們要保證程式能夠通過編譯,并且順利執行,必須要保證等号兩邊的類型是相容的。完全不相關的兩個類是不能夠出現在等号左右兩邊的,即使可以使用強制類型轉換通過編譯,在運作時依然會抛出異常。

于是我們就聯想到了子父類是否有可能進行相容呢?會有兩種情況:子類引用指向父類對象,父類引用指向子類對象,下面我們來一一讨論。

  • 子類引用指向父類對象為什麼無法使用

子類引用指向父類對象指的是:等号左側為子類型的聲明定義,等号右側為父類型的執行個體。首先,結論是這種用法是不存在的,我們從兩方面來分析原因。

第一個方面,是否符合邏輯?也就是是否會有某種需求,讓Java語言為開發者提供這樣一種用法?顯然是否定的,我們定義子類的目的就是為了擴充父類的功能,結果現在我們卻在用老舊的、功能貧乏的父類執行個體(等号右側)去滿足已經具備了強勁的、功能更為強大的子類聲明(等号左側)的需要,這顯然是不合理的。

另一方面,在程式運作時是否能夠辦到?如果我們真的寫出了相關的代碼,會要求我們添加強制轉換的語句,否則無法通過編譯,即使通過,在運作時也會提示無法進行類型轉換。這就相當于把一個隻能打電話發短信的老人機強制轉換為能安裝各種APP的智能機,這顯然是辦不到的。

  • 父類引用指向子類對象有什麼樣的意義

父類引用指向子類對象指的是:等号左側為父類型的定義,等号右側為子類型的執行個體。這種情況是會被經常使用的,類似的還有:接口指向實作類。那麼,這種用法應該如何解釋,又為什麼要有這樣的用法呢?

首先,我們先來了解一下這代表什麼含義,假如:父類為圖形,子類為矩形和圓形。這就好比我聲明了一個圖形對象,這個時候我們知道,可以調用出圖形類中定義的方法,由于圖形類是一個抽象類,是不能直接執行個體化的,我們隻能用他的兩個子類試試看。

public class Test{    public static void main(String[] args){        // figure1指向Rectangle執行個體        Figure figure1 = new Rectangle(5,10);        System.out.println(figure1.getC());// 得到矩形周長        System.out.println(figure1.getS());// 得到矩形面積        // figure2指向Circle執行個體        Figure figure2 = new Circle(5);        System.out.println(figure2.getC());// 得到圓形周長        System.out.println(figure2.getS());// 得到圓形面積    }}
           

從上面的結果來看,這好像和子類引用指向子類對象的執行效果沒什麼差別呀?但是需要注意此時使用的是父類的引用,差別就在于,如果我們在子類中定義了獨有的内容,是調用不到的。在上面已經解釋了運作效果以等号右側的執行個體為準,是以結果與直接建立的子類執行個體相同并不難了解。

重點要說明一下其中的含義:使用Figure(圖形)聲明,代表我現在隻知道是一個圖形,知道能執行哪些方法,如果再告知是一個矩形,那就能算出這個矩形的周長和面積;如果是一個圓形,那就能算出這個圓形的周長和面積。我們也可以這樣去描述:這個圖形是一個矩形或這個圖形是一個圓形。

如果從程式運作的角度去解釋,我們已經知道,子類對象在執行個體化時會先執行個體化父類對象,并且,如果子類重寫了父類的方法,父類的方法将會隐藏。如果我們用一個父類引用去指向一個子類對象,這就相當于對象執行個體很強大,但是我們隻能啟用部分的功能,但是有一個好處就是 相同的指令,不同的子類對象都能夠執行,并且會存在差異 。這就相當于一部老人機,隻具備打電話和發短信的功能,小米手機和魅族手機都屬于更新擴充後的智能機,當然保有手機最基本的通訊功能,這樣使用是沒問題的。

四、多态

學習了上面的内容後,其實你已經掌握了多态的用法,現在我們來明确總結一下。

1. 什麼是多态

多态指的是同一個父類,或同一個接口,發出了一個相同的指令(調用了同一個方法),由于具體執行的執行個體(子類對象或實作類對象)不同,而有不同的表現形态(執行效果)。

就像上面例子中的圖形一樣,自身是一個抽象類,其中存在一些抽象方法,具體的執行可以由子類對象來完成。對于抽象類的抽象方法,由于子類必須進行重寫,是以由子類去執行父類的抽象方法必然是多态的展現,對于其他的情況則未必構成多态,是以總結了以下三個必要條件。

2. 多态的必要條件

  • 存在子父類繼承關系
  • 子類重寫父類的方法
  • 父類引用指向子類對象

隻有滿足了這三個條件才能構成多态,這也就是文章前三點用這麼長的篇幅來鋪墊的原因。

3. 多态的優點

使用多态有多種好處,特别是一個抽象類有多個子類,或一個接口存在多個抽象類時,在進行參數傳遞時就會非常的靈活,在方法中隻需要定義一個父類型作為聲明,傳入的參數可以是父類型本身,也可以是對應的任意子類型對象。于是,多态的優點可以總結如下:

  • 降低耦合:隻需要與父類型産生關聯即可
  • 可維護性(繼承保證):隻需要添加或修改某一子類型即可,不會影響其他類
  • 可擴充性(多态保證):使用子類,可以對已有功能進行快速擴充
  • 靈活性
  • 接口性