物件導向程式設計有三個特徵,即封裝、繼承和多型。
封裝隱藏了類的內部實現機制,從而可以在不影響使用者的前提下改變類的內部結構,同時保護了資料。
繼承是為了重用父類程式碼,同時為實現多型性作準備。那麼什麼是多型呢?
方法的重寫、過載與動態連接構成多型性。Java之所以引入多型的概念,原因之一是它在類的繼承問題上和C++ 不同,後者允許多繼承,這確實給其帶來的非常強大的功能,但是複雜的繼承關係也給C++開發者帶來了更大的麻煩,為了規避風險,Java只允許單繼承,衍生類別與基礎類別間有IS-A的關係(即「貓」is a 「動物」)。這樣做雖然保證了繼承關係的簡單明了,但是勢必在功能上有很大的限制,所以,Java引入了多型性的概念以彌補這點的不足,此外,抽象類和介面也是解決單繼承規定限制的重要手段。同時,多型也是物件導向程式設計的精髓所在。
要理解多型性,首先要知道什麼是「向上轉型」。
我定義了一個子類別Cat,它繼承了Animal類,那麼後者就是前者是父類。我可以透過
Cat c = new Cat();
例項化一個Cat的物件,這個不難理解。但當我這樣定義時:
Animal a = new Cat();
這代表什麼意思呢?
很簡單,它表示我定義了一個Animal型別的引用,指向新建的Cat型別的物件。由於Cat是繼承自它的父類Animal,所以Animal型別的引用是可以指向Cat型別的物件的。那麼這樣做有什麼意義呢?因為子類別是對父類的一個改進和擴充,所以一般子類別在功能上較父類更強大,屬性較父類更獨特,
定義一個父類型別的引用指向一個子類別的物件既可以使用子類別強大的功能,又可以抽取父類的共性。
所以,父類型別的引用可以呼叫父類中定義的所有屬性和方法,而對於子類別中定義而父類中沒有的方法,它是無可奈何的;
同時,父類中的一個方法衹有在在父類中定義而在子類別中沒有重寫的情況下,才可以被父類型別的引用呼叫;
對於父類中定義的方法,如果子類別中重寫了該方法,那麼父類型別的引用將會呼叫子類別中的這個方法,這就是動態連接。
看下面這段程式:
class Father{
public void func1(){
func2();
}
//這是父類中的func2()方法,因為下面的子類別中重寫了該方法
//所以在父類型別的引用中呼叫時,這個方法將不再有效
//取而代之的是將呼叫子類別中重寫的func2()方法
public void func2(){
System.out.println("AAA");
}
}
class Child extends Father{
//func1(int i)是對func1()方法的一個過載
//由於在父類中沒有定義這個方法,所以它不能被父類型別的引用呼叫
//所以在下面的main方法中child.func1(68)是不對的
public void func1(int i){
System.out.println("BBB");
}
//func2()重寫了父類Father中的func2()方法
//如果父類型別的引用中呼叫了func2()方法,那麼必然是子類別中重寫的這個方法
public void func2(){
System.out.println("CCC");
}
}
public class PolymorphismTest {
public static void main(String[] args) {
Father child = new Child();
child.func1();//列印結果將會是什麼?
}
}
上面的程式是個很典型的多型的範例。子類別Child繼承了父類Father,並過載了父類的func1()方法,重寫了父類的func2()方法。過載後的func1(int i)和func1()不再是同一個方法,由於父類中沒有func1(int i),那麼,父類型別的引用child就不能呼叫func1(int i)方法。而子類別重寫了func2()方法,那麼父類型別的引用child在呼叫該方法時將會呼叫子類別中重寫的func2()。
那麼該程式將會列印出什麼樣的結果呢?
很顯然,應該是「CCC」。
對於多型,可以總結它為:
一、使用父類型別的引用指向子類別的物件;
二、該引用只能呼叫父類中定義的方法和變數;
三、如果子類別中重寫了父類中的一個方法,那麼在呼叫這個方法的時候,將會呼叫子類別中的這個方法;(動態連接、動態呼叫)
四、變數不能被重寫(覆蓋),」重寫「的概念只針對方法,如果在子類別中」重寫「了父類中的變數,那麼在編譯時會報錯。
****************************************************************************************************************************
多型詳解(整理)2008-09-03 19:29多型是透過:
1 介面 和 實現介面並覆蓋介面中同一方法的幾不同的類體現的
2 父類 和 繼承父類並覆蓋父類中同一方法的幾個不同子類別實現的.
一、基本概念
多型性:發送訊息給某個物件,讓該物件自行決定回應何種行為。
透過將子類別物件引用賦值給超類物件引用變數來實現動態方法呼叫。
java 的這種機制遵循一個原則:當超類物件引用變數引用子類別物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類別覆蓋的方法。
1. 如果a是類A的一個引用,那麼,a可以指向類A的一個例項,或者說指向類A的一個子類別。
2. 如果a是介面A的一個引用,那麼,a必須指向實現了介面A的一個類的例項。
二、Java多型性實現機制
SUN目前的JVM實現機制,類例項的引用就是指向一個控制代碼(handle)的指標,這個控制代碼是一對指標:
一個指標指向一張表格,實際上這個表格也有兩個指標(一個指標指向一個包含了物件的方法表,另外一個指向類物件,表明該物件所屬的型別);
另一個指標指向一塊從java堆中為分配出來記憶體空間。
三、總結
1、透過將子類別物件引用賦值給超類物件引用變數來實現動態方法呼叫。
DerivedC c2=new DerivedC();
BaseClass a1= c2; //BaseClass 基礎類別,DerivedC是繼承自BaseClass的子類別
a1.play(); //play()在BaseClass,DerivedC中均有定義,即子類別覆寫了該方法
分析:
* 為什麼子類別的型別的物件例項可以覆給超類引用?
自動實現向上轉型。透過該敘述,編譯器自動將子類別例項向上移動,成為通用型別BaseClass;
* a.play()將執行子類別還是父類定義的方法?
子類別的。在執行期期,將根據a這個物件引用實際的型別來取得對應的方法。所以才有多型性。一個基礎類別的物件引用,被賦予不同的子類別物件引用,執行該方法時,將表現出不同的行為。
在a1=c2的時候,仍然是存在兩個控制代碼,a1和c2,但是a1和c2擁有同一塊資料記憶體塊和不同的函式表。
2、不能把父類物件引用賦給子類別物件引用變數
BaseClass a2=new BaseClass();
DerivedC c1=a2;//出錯
在java裡面,向上轉型是自動進行的,但是向下轉型卻不是,需要我們自己定義強制進行。
c1=(DerivedC)a2; 進行強制轉化,也就是向下轉型.
3、記住一個很簡單又很複雜的規則,一個型別引用只能引用參考型別自身含有的方法和變數。
你可能說這個規則不對的,因為父類引用指向子類別物件的時候,最後執行的是子類別的方法的。
其實這並不矛盾,那是因為採用了後期繫結,動態執行的時候又根據型別去呼叫了子類別的方法。而假若子類別的這個方法在父類中並沒有定義,則會出錯。
例如,DerivedC類在繼承BaseClass中定義的函式外,還增加了幾個函式(例如 myFun())
分析:
當你使用父類引用指向子類別的時候,其實jvm已經使用了編譯器產生的型別訊息調整轉換了。
這裡你可以這樣理解,相當於把不是父類中含有的函式從虛擬函式表中設定為不可見的。注意有可能虛擬函式表中有些函式地址由於在子類別中已經被改寫了,所以物件虛擬函式表中虛擬函式專案地址已經被設定為子類別中完成的方法體的地址了。
4、Java與C++多型性的比較
jvm關於多型性支援解決方法是和c++中幾乎一樣的,
只是c++中編譯器很多是把型別訊息和虛擬函式訊息都放在一個虛擬函式表中,但是利用某種技術來區別。
Java把型別訊息和函式訊息分開放。Java中在繼承以後,子類別會重新設定自己的虛擬函式表,這個虛擬函式表中的專案有由兩部分組成。從父類繼承的虛擬函式和子類別自己的虛擬函式。
虛擬函式呼叫是經過虛擬函式表間接呼叫的,所以才得以實現多型的。
Java的所有函式,除了被宣告為final的,都是用後期繫結。
四. 1個行為,不同的物件,他們具體體現出來的方式不一樣,
例如: 方法過載 overloading 以及 方法重寫(覆蓋)override
class Human{
void run(){輸出 人在跑}
}
class Man extends Human{
void run(){輸出 男人在跑}
}
這個時候,同是跑,不同的物件,不一樣(這個是方法覆蓋的範例)
class Test{
void out(String str){輸出 str}
void out(int i){輸出 i}
}
這個範例是方法過載,方法名相同,引數表不同
ok,明白了這些還不夠,還用人在跑舉例
Human ahuman=new Man();
這樣我等於例項化了一個Man的物件,並宣告了一個Human的引用,讓它去指向Man這個物件
意思是說,把 Man這個物件當 Human看了.
例如去動物園,你看見了一個動物,不知道它是什麼, "這是什麼動物? " "這是大熊貓! "
這2句話,就是最好的證明,因為不知道它是大熊貓,但知道它的父類是動物,所以,
這個大熊貓物件,你把它當成其父類 動物看,這樣子合情合理.
這種方式下要注意 new Man();的確例項化了Man物件,所以 ahuman.run()這個方法 輸出的 是 "男人在跑 "
如果在子類別 Man下你 寫了一些它獨有的方法 例如 eat(),而Human沒有這個方法,
在呼叫eat方法時,一定要注意 強制型轉 ((Man)ahuman).eat(),這樣才可以...
對介面來說,情況是類似的...
例項:
package domatic;
//定義超類superA
class superA {
int i = 100;
void fun(int j) {
j = i;
System.out.println("This is superA");
}
}
// 定義superA的子類別subB
class subB extends superA {
int m = 1;
void fun(int aa) {
System.out.println("This is subB");
}
}
// 定義superA的子類別subC
class subC extends superA {
int n = 1;
void fun(int cc) {
System.out.println("This is subC");
}
}
class Test {
public static void main(String[] args) {
superA a = new superA();
subB b = new subB();
subC c = new subC();
a = b;
a.fun(100);
a = c;
a.fun(200);
}
}
/*
* 上述程式碼中subB和subC是超類superA的子類別,我們在類Test中宣告了3個引用變數a, b,
* c,透過將子類別物件引用賦值給超類物件引用變數來實現動態方法呼叫。也許有人會問:
* "為什麼(1)和(2)不輸出:This is superA"。
* java的這種機制遵循一個原則:當超類物件引用變數引用子類別物件時,
* 被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,
* 但是這個被呼叫的方法必須是在超類中定義過的,
* 也就是說被子類別覆蓋的方法。
* 所以,不要被上例中(1)和(2)所迷惑,雖然寫成a.fun(),但是由於(1)中的a被b賦值,
* 指向了子類別subB的一個例項,因而(1)所呼叫的fun()實際上是子類別subB的成員方法fun(),
* 它覆蓋了超類superA的成員方法fun();同樣(2)呼叫的是子類別subC的成員方法fun()。
* 另外,如果子類別繼承的超類是一個抽象類,雖然抽象類不能透過new運算子例項化,
* 但是可以建立抽象類的物件引用指向子類別物件,以實現執行期多型性。具體的實現方法同上例。
* 不過,抽象類的子類別必須覆蓋實現超類中的所有的抽象方法,
* 否則子類別必須被abstract修飾字修飾,當然也就不能被例項化了
*/
以上大多數是以子類別覆蓋父類的方法實現多型.下面是另一種實現多型的方法-----------重寫父類方法
1.JAVA里沒有多繼承,一個類之能有一個父類。而繼承的表現就是多型。一個父類可以有多個子類別,而在子類別里可以重寫父類的方法(例如方法print()),這樣每個子類別里重寫的程式碼不一樣,自然表現形式就不一樣。這樣用父類的變數去引用不同的子類別,在呼叫這個相同的方法print()的時候得到的結果和表現形式就不一樣了,這就是多型,相同的訊息(也就是呼叫相同的方法)會有不同的結果。舉例說明:
//父類
public class Father{
//父類有一個打孩子方法
public void hitChild(){
}
}
//子類別1
public class Son1 extends Father{
//重寫父類打孩子方法
public void hitChild(){
System.out.println("為什麼打我?我做錯什麼了!");
}
}
//子類別2
public class Son2 extends Father{
//重寫父類打孩子方法
public void hitChild(){
System.out.println("我知道錯了,別打了!");
}
}
//子類別3
public class Son3 extends Father{
//重寫父類打孩子方法
public void hitChild(){
System.out.println("我跑,你打不著!");
}
}
//測試類
public class Test{
public static void main(String args[]){
Father father;
father = new Son1();
father.hitChild();
father = new Son2();
father.hitChild();
father = new Son3();
father.hitChild();
}
}
都呼叫了相同的方法,出現了不同的結果!這就是多型的表現!