個人學習記錄,歡迎大家指導
什麼是多型?
一個引用變數,它可以引用任何子類的物件,這個引用變數是多型的。
繫結
將一個方法呼叫與對應方法主體關聯起來被稱為繫結。(也就是,執行一條方法呼叫語句時所對應執行的方法體,叫做該方法體和這條方法呼叫語句繫結了)
動態繫結
來看一段程式碼
public class Parent {
public void m(){
System.out.println("父類方法執行");
}
public static void main(String[] arg){
List<Parent> list = Arrays.asList(new A(),new B(),new C());
for(Parent p : list){
p.m();
}
}
}
class A extends Parent{
public void m(){
System.out.println("A類方法執行");
}
}
class B extends Parent{
public void m(){
System.out.println("B類方法執行");
}
}
class C extends Parent{
public void m(){
System.out.println("C類方法執行");
}
}
output:
A類方法執行
B類方法執行
C類方法執行
何為動態繫結?
上述程式碼中,第一次迴圈,p變數接受一個A物件,p.m()語句呼叫A的m()方法,第二次迴圈時,p變數接受一個B物件,p.m()語句呼叫B的m()方法,第三次迴圈時,p變數接受一個C物件,p.m()語句呼叫C的m()方法,3次迴圈都是執行的p.m()語句,每次迴圈p.m()語句繫結的方法體不一樣,這種執行時根據變數p實際引用物件動態繫結方法體的方式,叫做動態繫結。動態繫結是為了執行子類重寫的方法。
靜態繫結
對於static、final、private修飾的方法,以及構造方法,這些方法不可被子類重寫,它們採用靜態繫結。
方法呼叫的具體過程
看一段程式碼
public class Fu {
public void m(Object obj){
System.out.println("父類Object方法執行");
}
public static void main(String[] args){
Fu f = new Zi();
f.m(1);
}
}
class Zi extends Fu{
public void m(Object obj){
System.out.println("子類Object方法執行");
}
public void m(Integer i){
System.out.println("子類Integer方法執行");
}
}
output:
子類Object方法執行
為啥選擇子類的m(Object)方法執行,而不是m(Integer)方法執行呢,你肯定想說因為向上轉型,不能執行子類特有的方法,確實是這樣,但為什麼呢?
- 來看一下方法呼叫的過程
編譯期:
1、編譯器根據變數f宣告的型別Fu,去Fu類中查詢名字為m的方法。
2、此時可能找到多個名為m的方法,因為方法是可以過載的,編譯器再根據呼叫方法時提供的引數型別,去找到最匹配的方法,在這裡是m(Object)方法,
這個過程叫做過載解析。過載解析後,編譯器獲得了方法的簽名(方法名+形參型別)。
3、如果方法是被static、final、private修飾的,該方法不能被重寫,f.m(1)執行時,無論f引用什麼子類物件,都是執行的父類的方法,此時可以確定執行時無論f的物件是什麼都是執行父類中的方法,繫結Fu類中的m(Object)方法。此為前期繫結,也叫靜態繫結,是編譯期時發生的繫結。
4、如果方法不被static、final、private修飾,編譯器採用動態繫結,記錄方法簽名m(Object)。
執行期:
執行f.m(1)位元組碼時
1、如果是靜態繫結,直接執行繫結的方法,不關心物件型別。
2、如果是動態繫結,編譯期只確定了方法簽名,方法所在類未確定,根據f引用物件的實際型別,去該型別中找到簽名為m(Object)的方法,執行該方法,此時執行時才把方法呼叫與具體方法體關聯起來。
動態繫結就是執行的時候才決定執行哪個方法體,靜態繫結在編譯期就確定了要執行的方法體。
動態繫結在編譯期確定執行的方法簽名,在執行期確定執行哪個類的方法,實現自由執行子類重寫的方法。靜態繫結在編譯期決定執行哪個類中的哪個方法,因為這些方法不能被重寫,子類物件呼叫也是呼叫的父類中的,可以明確方法體,對於這類不能重寫的方法,採用靜態繫結效能更好,雖然採用動態繫結也能實現。
一個靜態繫結的例子
public class Fu {
private void m(){
System.out.println("父類private方法執行");
}
public static void main(String[] args){
Fu f = new Zi();
f.m();
}
}
public class Zi extends Fu{
public void m(){
System.out.println("子類public方法執行");
}
public static void main(String[] args){
Fu f = new Zi();
// f.m(); 無法編譯
}
}
output:
父類private方法執行
補充一個繼承泛型類的例子
public class GenericClazz<T> {
public void m(T t){
System.out.println("泛型類中m()方法執行");
}
}
public class Zi extends GenericClazz<Integer>{
public void m(Integer i){
System.out.println("子類中m()方法執行");
}
// public void m(Object i){
// System.out.println("子類中m()方法執行");
// } 無法編譯
public static void main(String[] args){
GenericClazz f = new Zi();
f.m(1);
}
}
output:
子類中m()方法執行
泛型類中,由於有泛型擦除,T實際會被在編譯時替換為Object型別,那麼Zi類繼承GenericClazz類就是繼承的m(Object)方法,而m(Integer)是對m(Object)的過載而不是重寫,在用f.m(1)呼叫時,按照上面的理論,執行時應該是呼叫的Zi類中的m(Object)方法,而m(Object)子類沒有重寫,應該走父類語句,但是這裡卻走了子類的m(Integer)方法,原因是編譯器自動在子類做了m(Object)方法的重寫,會執行類似下面的語句:
public void m(Object t){
m((Integer) t);
}
讓m(Integer)看起來就像是重寫的父類的m(Integer)一樣,並且還關閉了子類對m(Object)的手動重寫,可以看到上面手動重寫m(Object)編譯是報錯的。