JAVA多型理解(包含他人經典例子)
引言:理解JAVA多型應先理解JAVA繼承、封裝。
一、什麼是多型
面向物件程式設計有三個特徵,即封裝、繼承和多型。
1. 封裝隱藏了類的內部實現機制,從而可以在不影響使用者的前提下改變類的內部結構,同時保護了資料。
2. 繼承是為了重用父類程式碼,同時為實現多型性作準備。那麼什麼是多型呢?
3.方法的重寫、過載與動態連線構成多型性。Java之所以引入多型的概念,原因之一是它在類的繼承問題上和C++不同,後者允許多繼承,這確實給其帶來非常強大的功能,但是複雜的繼承關係也給C++開發者帶來了更大的麻煩,為了規避風險,Java只允許單繼承,派生類與基類間有IS-A的關係(即“貓”is a “動
二、理解多型性
①要理解多型性,首先要知道什麼是“向上轉型”。
我定義了一個子類Cat,它繼承了Animal類,那麼後者就是前者是父類。
我可以通過 Cat c = new Cat(); 例項化一個Cat的物件,但當我這樣定義時:Animal a = new Cat(); 這代表什麼意思呢?
很簡單,它表示我定義了一個Animal型別的引用,指向新建的Cat型別的物件。由於Cat是繼承自它的父類Animal,所以Animal型別的引用是可以指向
因為子類是對父類的一個改進和擴充,所以一般子類在功能上較父類更強大,屬性較父類更獨特,定義一個父類型別的引用指向一個子類的物件既可以使用子類強大的功能,又可以抽取父類的共性。 所以,父類型別的引用可以呼叫父類中定義的所有屬性和方法,而對於子類中定義而父類中沒有的方法,它是無可奈何的;同時,父類中的一個方法只有在在父類中定義而在子類中沒有重寫的情況下,才可以被父類型別的引用呼叫; 對於父類中定義的方法,如果子類中重寫了該方法,那麼父類型別的引用將會呼叫子類中的這個方法,這就是動態連線。
②例項程式碼:
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”。
③小結
1.使用父類型別的引用指向子類的物件;
2.該引用只能呼叫父類中定義的方法和變數;
3.如果子類中重寫了父類中的一個方法,那麼在呼叫這個方法的時候,將會呼叫子類中的這個方法;(動態連線、動態呼叫)
4.變數不能被重寫(覆蓋),”重寫“的概念只針對方法,如果在子類中”重寫“了父類中的變數,那麼在編譯時會報錯。(灰色部分觀點有瑕疵:不會在編譯時報錯,只是重寫不是對變數而言的)
eg:
class ParentClass {
public String AAA = "111";public void say(){System.out.println(AAA);}}class SubClass extends ParentClass{public String AAA = "777";}class TestClass {public static void main(String args[]){SubClass sub = new SubClass();sub.say();}}執行結果為:111
三、經典例子(請耐心看完,看完後豁然開朗,實在不想看可跳過)
</pre></p><p><pre name="code" class="java">class A ...{
public String show(D obj)...{
return ("A and D");
}
public String show(A obj)...{
return ("A and A");
}
}
class B extends A...{
public String show(B obj)...{
return ("B and B");
}
public String show(A obj)...{
return ("B and A");
}
}
class C extends B...{}
class D extends B...{}
問題如下:A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println(a1.show(b)); ①
System.out.println(a1.show(c)); ②
System.out.println(a1.show(d)); ③
System.out.println(a2.show(b)); ④
System.out.println(a2.show(c)); ⑤
System.out.println(a2.show(d)); ⑥
System.out.println(b.show(b)); ⑦
System.out.println(b.show(c)); ⑧
System.out.println(b.show(d)); ⑨
結果如下:
① A and A
② A and A
③ A and D
④ B and A
⑤ B and A
⑥ A and D
⑦ B and B
⑧ B and B
⑨ A and D
分析:
①②③比較好理解,一般不會出錯。④⑤就有點糊塗了,為什麼輸出的不是"B and B”呢?!!先來回顧一下多型性。
執行時多型性是面向物件程式設計程式碼重用的一個最強大機制,Java多型性的概念也可以被說成“一個介面,多個方法”。Java實現執行時多型性的基礎是動態方法排程,它是一種在執行時而不是在編譯期呼叫過載方法的機制。
方法的重寫Overriding和過載Overloading是Java多型性的不同表現。重寫Overriding是父類與子類之間多型性的一種表現,過載Overloading是一個類中多型性的一種表現。如果在子類中定義某方法與其父類有相同的名稱和引數,我們說該方法被重寫(Overriding)。子類的物件使用這個方法時,將呼叫子類中的定義,對它而言,父類中的定義如同被“遮蔽”了。如果在一個類中定義了多個同名的方法,它們或有不同的引數個數或有不同的引數型別,則稱為方法的重載(Overloading)。Overloaded的方法是可以改變返回值的型別。方法的重寫Overriding和過載Overloading是Java多型性的不同表現。重寫Overriding是父類與子類之間多型性的一種表現,過載Overloading是一個類中Java多型性的一種表現。如果在子類中定義某方法與其父類有相同的名稱和引數,我們說該方法被重寫 (Overriding)。子類的物件使用這個方法時,將呼叫子類中的定義,對它而言,父類中的定義如同被“遮蔽”了。如果在一個類中定義了多個同名的方法,它們或有不同的引數個數或有不同的引數型別,則稱為方法的過載(Overloading)。Overloaded的方法是可以改變返回值的型別。當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。 (但是如果強制把超類轉換成子類的話,就可以呼叫子類中新新增而超類沒有的方法了。) 此處灰色部分觀點不正確:子類不能強制轉換成父類。。。。。。
好了,先溫習到這裡,言歸正傳!實際上這裡涉及方法呼叫的優先問題 ,優先順序由高到低依次為:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。讓我們來看看它是怎麼工作的。
比如④,a2.show(b),a2是一個引用變數,型別為A,則this為a2,b是B的一個例項,於是它到類A裡面找show(B obj)方法,沒有找到,於是到A的super(超類)找,而A沒有超類,因此轉到第三優先順序this.show((super)O),this仍然是a2,這裡O為B,(super)O即(super)B即A,因此它到類A裡面找show(A obj)的方法,類A有這個方法,但是由於a2引用的是類B的一個物件,B覆蓋了A的show(A obj)方法,因此最終鎖定到類B的show(A obj),輸出為"B and A”。再比如⑧,b.show(c),b是一個引用變數,型別為B,則this為b,c是C的一個例項,於是它到類B找show(C obj)方法,沒有找到,轉而到B的超類A裡面找,A裡面也沒有,因此也轉到第三優先順序this.show((super)O),this為b,O為C,(super)O即(super)C即B,因此它到B裡面找show(B obj)方法,找到了,由於b引用的是類B的一個物件,因此直接鎖定到類B的show(B obj),輸出為"B and B”。
按照上面的方法,可以正確得到其他的結果。
問題還要繼續,現在我們再來看上面的分析過程是怎麼體現出藍色字型那句話的內涵的。它說:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。還是拿a2.show(b)來說吧。a2是一個引用變數,型別為A,它引用的是B的一個物件,因此這句話的意思是由B來決定呼叫的是哪個方法。因此應該呼叫B的show(B obj)從而輸出"B and B”才對。但是為什麼跟前面的分析得到的結果不相符呢?!問題在於我們不要忽略了藍色字型的後半部分,那裡特別指明:這個被呼叫的方法必須是在超類中定義過的,也就是被子類覆蓋的方法。B裡面的show(B obj)在超類A中有定義嗎?沒有!那就更談不上被覆蓋了。實際上這句話隱藏了一條資訊:它仍然是按照方法呼叫的優先順序來確定的。它在類A中找到了show(A obj),如果子類B沒有覆蓋show(A obj)方法,那麼它就呼叫A的show(A obj)(由於B繼承A,雖然沒有覆蓋這個方法,但從超類A那裡繼承了這個方法,從某種意義上說,還是由B確定呼叫的方法,只是方法是在A中實現而已);現在子類B覆蓋了show(A obj),因此它最終鎖定到B的show(A obj)。這就是那句話的意義所在,到這裡,我們可以清晰的理解Java的多型性了。
四、總結
①多型性體現在方法的重寫,過載以及動態連結。重寫體現在子類的多型,過載體現了自身類中方法的多型,動態連結就是使用父類型別的引用指向子類的物件,該引用只能呼叫父類中定義的方法和變數,程式執行時才決定執行什麼方法。
☆☆☆☆☆特別注意:
②當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。
③一個型別引用只能引用引用型別自身含有的方法和變數。