Java面向對象編程三大特徵

多態是面向對象編程的三大特徵之一,是面向對象思想的終極體現之一。在理解多態之前需要先掌握繼承、重寫、父類引用指向子類對象的相關概念

一、抽象類

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

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

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

1. 抽象與抽象類

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

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

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

  • 抽象方法只能存在於抽象類中(接口在另外的文章中討論)
  • 抽象類無法被直接實例化(匿名內部類的用法暫不做討論)

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

Java面向對象編程三大特徵 - 多態

2. 抽象類的特點

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

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

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

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

3. 天生的父類:抽象類

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

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

二、重寫

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

1. 重寫與重載的區別

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

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

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

<code>// 定義矩形類
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("矩形");
}
}/<code>
<code>// 定義圓形類
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("圓形");
}
}/<code>

2. 方法重寫的規則

  • 重寫的標識為@Override
  • 方法的重寫發生在子類或者接口的實現類中
  • 被final聲明的方法不能被重寫
  • 被static聲明的方法不能被重寫,只能聲明同結構的靜態方法,但是此時不構成重寫
  • 受限於權限修飾符,子類可能只能重寫部分父類中的方法

3. 父類方法的顯式調用

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

比較規範的說法是:通過子類實例無法直接調用到父類中的同名方法了,但是在內存中依然存在著父類方法的結構,只不過訪問不到而已。另外,我們同樣可以在子類中顯式的調用出父類方法,這要用到super關鍵字。

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

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

<code>// 定義抽象類:圖形類
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("這是一個圖形");
}
}/<code>
<code>// 定義矩形類
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("矩形");
}
}/<code>
<code>// 定義圓形類
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("圓形");
}
}/<code>

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

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

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

如果父類是一個抽象類,則在等號右側不能直接使用new加構造方法的方式實例化,如果一定要得到父類實例,就要使用匿名內部類的用法,這裡不做討論。

如果父類是一個普通類,那麼我們在初始化時,等號左側為父類型引用,等號右側為父類型對象(實例),這個時候其實和我們去創建一個類的對象並沒有什麼分別,不需要想著他是某某類的父類,因為 此時他不會和任何子類產生關係,只是一個默認繼承了Object類的普通類 ,正常使用就好,能調用出的內容也都是父類中已定義的。

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

在進行子類實例化時,由於在子類的定義中繼承了父類,所以在創建子類對象時,會先一步創建父類對象。在進行調用時,根據權限修飾符,可以調用出子類及父類中可訪問的屬性和方法。

<code>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());// 得到圓形面積
}
}/<code>

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

在剛開始學習編程時,我們接觸了基本數據類型,可以直接用關鍵字聲明,定義變量賦值後使用,並不需要使用new關鍵字。對於引用與對象的關係可以先參考之前的文章回顧一下: Java中的基本操作單元 - 類和對象 。在這裡我們重點要說明的是:等號左側的引用部分,與等號右側的部分在程序運行層面有怎樣的關聯。

與基本數據類型不同,在類中可以定義各種屬性和方法,使用時也需要先創建對象。等號左側的部分依然是一個類型的聲明,未賦值時雖然默認情況下是null,但在程序編譯運行時,也會在棧中進行存儲,記錄了相應的結構信息,他所指向的對象必須是一個和它 兼容 的類型。

類的聲明引用存放在棧中,實例化得到的對象存放在堆中。

  • 在代碼編寫階段,能夠調用出的內容以等號左側類型為準
  • 在程序運行階段,具體的的執行效果以等號右側實例為準

下圖為引用與實例在內存中的關係示意圖,有關於Java對象在內存中的分佈將在另外的文章中說明:

Java面向對象編程三大特徵 - 多態

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

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

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

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

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

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

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

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

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

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

<code>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());// 得到圓形面積
}
}/<code>

從上面的結果來看,這好像和子類引用指向子類對象的執行效果沒什麼區別呀?但是需要注意此時使用的是父類的引用,區別就在於,如果我們在子類中定義了獨有的內容,是調用不到的。在上面已經解釋了運行效果以等號右側的實例為準,所以結果與直接創建的子類實例相同並不難理解。

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

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

四、多態

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

1. 什麼是多態

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

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

2. 多態的必要條件

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

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

3. 多態的優點

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

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


分享到:


相關文章: