天天看點

【Java】多态淺析

前言

在面向對象程式設計語言中,多态是繼資料抽象和繼承之後的第三種基本特性。多态的含義是什麼,有什麼作用以及在Java中是怎麼實作的?下面将做介紹。

什麼是多态

簡單點說就是“一個接口,多種實作”,不同類對同一操作展現出不同效果。

設想有一個性質:一個引用變量所指向的确切類型和該引用變量調用的方法是哪個類中的,這個兩個問題在編譯期間是不确定的,在程式運作期間才可确定。

于是,一份代碼就可以适用于多個不同的類,隻要這份代碼中有一個引用變量可以指向這些不同的類的對象。

在程式運作期間,就可以動态選擇多個不同的對象或者多個不同的方法運作,這就是多态性。

也可以簡單的使用Java核心卷中的一句話說:一個引用變量可以訓示多種實際類型的現象被稱為多态。

要什麼樣的引用變量才可以指向多種不同類的對象呢?那就需要是基類引用變量。這就涉及到

向上轉型

向上轉型

簡單一句話就是:基類(父類)引用指向子類對象。向上轉型這個術語屬于也是有曆史原因的,以傳統的類繼承圖的繪制方法為基礎:将根置于頁面的頂端,然後逐漸向下。

為什麼基類引用就可以指向子類對象呢?

這可以以生活中的一個例子來說,狗是對所有品種狗的統稱,具體的品種又有哈士奇,拉布拉多犬等。假設我們在路上遇見了一隻不知道名字的狗(比如牧羊犬),我們也許會說:那兒有一隻狗。此時,我們就做了向上轉型,以狗指向了具體的牧羊犬。

以範圍較大的基類引用去指向範圍小的子類。這是以生活中的例子解釋,在語言層面其實也可以說明:子類是繼承基類而來,是以基類中的所有方法子類也有,可以發送給基類的消息同樣也可以發送給子類。使用基類引用也就可以指向子類對象調用這些方法。

可以使用基類引用指向子類對象,那麼可不可以使用子類引用指向基類對象呢?是可以的,這就叫做向下轉型,但是這存在風險。因為子類中可能會有新增方法,而基類中是沒有這些方法的,若是轉為基類型後再調用這些方法就會抛出

ClassCastException

異常。

介紹了什麼是多态,那麼就了解它的作用有什麼。

多态的作用

多态的一個好處就是可以實作統一管理,需要注意父類不能調用子類特有的方法即父類中沒有的方法子類有的方法,若要調用需要向下轉型。

多态如何實作

Java中實作多态的三個要求為:

  • 要有繼承關系
  • 方法要被重寫
  • 基類引用指向子類對象

可以看一個簡單的多态例子:

class Animal{
	public void eat() { System.out.println("吃東西"); }
}

class Dog extends Animal{
	public void eat() { System.out.println("吃狗糧");}
}

class Cat extends Animal{
	public void eat() { System.out.println("吃小魚幹");}
}

public class PloyTest {
	//使用父類引用指向子類對象 調用子類中被重寫的方法
	public static void printEatingFood(Animal a) { a.eat();}
	
	public static void main(String[] args) {
		printEatingFood(new Dog());
		printEatingFood(new Cat());
	}
}
/*
output:
吃狗糧
吃小魚幹
*/
           

當我們傳入Dog對象時,a.eat()調用的是Dog類中被重寫的eat方法;傳入Cat對象時,a.eat()調用的是Cat類中被重寫的方法。

程式在運作過程中,依據我們傳入的對象自動地為我們尋找到正确的方法調用。而這也叫做動态綁定。

動态綁定

《Java程式設計思想》上這樣說:運作時父類引用根據其指向的對象,綁定到相應的對象方法上。

那麼這個過程的具體實作是怎樣的呢?《Java核心技術卷1》上是這樣解釋的解釋:

先是要清楚調用對象方法的過程:

  1. (搜尋過程)編譯器檢視對象的聲明類型和方法名。可能會有同名的重載方法。

若是調用x.f(param)

獲得目前類和超類中為public的名為f的方法,即獲可能被調用的候選方法

  1. (比對過程)然後,編譯器檢視調用方法時提供的參數類型,與候選方法進行比對,此過程也就是重載解析。

完全比對,則選擇調用該方法,這其中還會存在類型轉換,是以過程會比較複雜。最後若是沒有找到比對的,編譯器則會報錯。

靜态綁定:

若方法是private、 static、 final或者構造器,編譯器會知道調用哪個方法,這種方式為靜态綁定。

動态綁定:

依賴于隐式參數的實際類型,在運作時才可以确定調用方法,則為動态綁定。

當程式運作,并且采用動态綁定調用方法時,虛拟機會調用與x所引用對象的實際類型最合适的那個類的方法。并且為了避免每次搜尋浪費時間,虛拟機會為每個類建立一個方法表。其中包含所有方法的簽名(方法名+形參)和實際調用的方法(包括繼承來方法)。

一些陷阱和建議

域和靜态方法

我們需要注意隻有普通方法調用才可以是多态的,對域的通路将在編譯時期進行解析。

如下面這個例子:

class Super{
	public int field = 0;
	public int getField() { return field;}
}

class Sub extends Super{
	public int field = 1;
	public int getField() { return field;}
	public int getSuperField() { return super.getField();}
}

public class FieldAccess {
	public static void main(String[] args) {
		Super sup = new Sub();
		System.out.println("sup.field="+sup.field+" sup.getField()="+sup.getField());
		
		Sub sub = new Sub();
		System.out.println("sub.getField="+sub.field+" sub.getField()="+sub.getField()+
				" sub.getSuperField()="+sub.getSuperField());
	}
}
/*
output:
sup.field=0 sup.getField()=1
sub.getField=1 sub.getField()=1 sub.getSuperField()=0
*/
           

當使用父類Super的引用sup指向子類Sub類對象,輸出域,發現是父類的值。是以,域的通路是編譯器解析,不是多态的。

如果某個方法是靜态的,它的行為也不具有多态性。

class StaticSuper{
	public static String staticGet() {
		return "Base staticGet()";
	}
	
	public String dynamicGet(){
		return "Base dynamicGet()";
	}
}

class StaticSub extends StaticSuper{
	public static String staticGet() {
		return "Derived staticGet()";
	}
	
	public String dynamicGet() {
		return "Derived dynamicGet()";
	}
}

public class OverloadingTest {
	public static void main(String[] args) {
		StaticSuper sup = new StaticSub();	//向上轉型
		System.out.println(sup.staticGet());
		System.out.println(sup.dynamicGet());
	}
}
/*
output:
Base staticGet()
Derived dynamicGet()
*/
           

由輸出可以看出,靜态方法是不具有多态性的。靜态方法是與類,而非與單個的對象相關聯的。

小結

簡要介紹了對于多态的了解,其中存在的不足,希望各位看官不吝賜教。

參考:

[1] Eckel B. Java程式設計思想(第四版)[M]. 北京: 機械工業出版社, 2007

[2] Cay S,Horstmann. Java核心技術 卷Ⅰ(第9版)[M]. 北京: 機械工業出版社, 2014.

[3] Jian_Yun_Rui. Java多态性了解,好處及精典執行個體[EB/OL]. /2019-02-19. https://blog.csdn.net/Jian_Yun_Rui/article/details/52937791.

每天進步一點點,不要停止前進的腳步~