一、面向對象最核心的機制——動態綁定,也叫多態
1.1.通過下面的例子理解動態綁定,即多態
1 package javastudy.summary; 2 3 class Animal { 4 /** 5 * 聲明一個私有的成員變量name。 6 */ 7 private String name; 8 9 /** 10 * 在Animal類自定義的構造方法 11 * @param name 12 */ 13 Animal(String name) { 14 this.name = name; 15 } 16 17 /** 18 * 在Animal類里面自定義一個方法enjoy 19 */ 20 public void enjoy() { 21 System.out.println("動物的叫聲……"); 22 } 23 } 24 25 /** 26 * 子類Cat從父類Animal繼承下來,Cat類擁有了Animal類所有的屬性和方法。 27 * @author gacl 28 * 29 */ 30 class Cat extends Animal { 31 /** 32 * 在子類Cat里面定義自己的私有成員變量 33 */ 34 private String eyesColor; 35 36 /** 37 * 在子類Cat里面定義Cat類的構造方法 38 * @param n 39 * @param c 40 */ 41 Cat(String n, String c) { 42 /** 43 * 在構造方法的實現里面首先使用super調用父類Animal的構造方法Animal(String name)。 44 * 把子類對象里面的父類對象先造出來。 45 */ 46 super(n); 47 eyesColor = c; 48 } 49 50 /** 51 * 子類Cat對從父類Animal繼承下來的enjoy方法不滿意,在這里重寫了enjoy方法。 52 */ 53 public void enjoy() { 54 System.out.println("我養的貓高興地叫了一聲……"); 55 } 56 } 57 58 /** 59 * 子類Dog從父類Animal繼承下來,Dog類擁有了Animal類所有的屬性和方法。 60 * @author gacl 61 * 62 */ 63 class Dog extends Animal { 64 /** 65 * 在子類Dog里面定義自己的私有成員變量 66 */ 67 private String furColor; 68 69 /** 70 * 在子類Dog里面定義Dog類的構造方法 71 * @param n 72 * @param c 73 */ 74 Dog(String n, String c) { 75 /** 76 * 在構造方法的實現里面首先使用super調用父類Animal的構造方法Animal(String name)。 77 * 把子類對象里面的父類對象先造出來。 78 */ 79 super(n); 80 furColor = c; 81 } 82 83 /** 84 * 子類Dog對從父類Animal繼承下來的enjoy方法不滿意,在這里重寫了enjoy方法。 85 */ 86 public void enjoy() { 87 System.out.println("我養的狗高興地叫了一聲……"); 88 } 89 } 90 91 /** 92 * 子類Bird從父類Animal繼承下來,Bird類擁有Animal類所有的屬性和方法 93 * @author gacl 94 * 95 */ 96 class Bird extends Animal { 97 /** 98 * 在子類Bird里面定義Bird類的構造方法 99 */ 100 Bird() { 101 /** 102 * 在構造方法的實現里面首先使用super調用父類Animal的構造方法Animal(String name)。 103 * 把子類對象里面的父類對象先造出來。 104 */ 105 super("bird"); 106 } 107 108 /** 109 * 子類Bird對從父類Animal繼承下來的enjoy方法不滿意,在這里重寫了enjoy方法。 110 */ 111 public void enjoy() { 112 System.out.println("我養的鳥高興地叫了一聲……"); 113 } 114 } 115 116 /** 117 * 定義一個類Lady(女士) 118 * @author gacl 119 * 120 */ 121 class Lady { 122 /** 123 * 定義Lady類的私有成員變量name和pet 124 */ 125 private String name; 126 private Animal pet; 127 128 /** 129 * 在Lady類里面定義自己的構造方法Lady(), 130 * 這個構造方法有兩個參數,分別為String類型的name和Animal類型的pet, 131 * 這里的第二個參數設置成Animal類型可以給我們的程序帶來最大的靈活性, 132 * 因為作為養寵物來說,可以養貓,養狗,養鳥,只要是你喜歡的都可以養, 133 * 因此把它設置為父類對象的引用最為靈活。 134 * 因為這個Animal類型的參數是父類對象的引用類型,因此當我們傳參數的時候, 135 * 可以把這個父類的子類對象傳過去,即傳Dog、Cat和Bird等都可以。 136 * @param name 137 * @param pet 138 */ 139 Lady(String name, Animal pet) { 140 this.name = name; 141 this.pet = pet; 142 } 143 144 /** 145 * 在Lady類里面自定義一個方法myPetEnjoy() 146 * 方法體內是讓Lady對象養的寵物自己調用自己的enjoy()方法發出自己的叫聲。 147 */ 148 public void myPetEnjoy() { 149 pet.enjoy(); 150 } 151 } 152 153 public class TestPolymoph { 154 public static void main(String args[]) { 155 /** 156 * 在堆內存里面new了一只藍貓對象出來,這個藍貓對象里面包含有一個父類對象Animal。 157 */ 158 Cat c = new Cat("Catname", "blue"); 159 /** 160 * 在堆內存里面new了一只黑狗對象出來,這個黑狗對象里面包含有一個父類對象Animal。 161 */ 162 Dog d = new Dog("Dogname", "black"); 163 /** 164 * 在堆內存里面new了一只小鳥對象出來,這個小鳥對象里面包含有一個父類對象Animal。 165 */ 166 Bird b = new Bird(); 167 168 /** 169 * 在堆內存里面new出來3個小姑娘,名字分別是l1,l2,l3。 170 * l1養了一只寵物是c(Cat),l2養了一只寵物是d(Dog),l3養了一只寵物是b(Bird)。 171 * 注意:調用Lady類的構造方法時,傳遞過來的c,d,b是當成Animal來傳遞的, 172 * 因此使用c,d,b這三個引用對象只能訪問父類Animal里面的enjoy()方法。 173 */ 174 Lady l1 = new Lady("l1", c); 175 Lady l2 = new Lady("l2", d); 176 Lady l3 = new Lady("l3", b); 177 /** 178 * 這三個小姑娘都調用myPetEnjoy()方法使自己養的寵物高興地叫起來。 179 */ 180 l1.myPetEnjoy(); 181 l2.myPetEnjoy(); 182 l3.myPetEnjoy(); 183 } 184 }
運行結果:
1.2.畫內存圖理解動態綁定(多態)
首先從main方法的第一句話開始分析:
Cat c = new Cat("Catname","blue");
程序執行到這里,棧空間里有一個變量c,c里面裝着一系列的值,通過這些值可以找到位於堆內存里面new出來的Cat對象。因此c是Cat對象的一個引用,通過c可以看到這個Cat對象的全部。c指向new出來的Cat對象。在new這個Cat對象的時候,調用了Cat對象的構造方法Cat(String n,String c),定義如下:
Cat(String n,String c){
super(n);
eyesColor=c;
}
因此在構造子類對象時首先使用父類對象的引用super調用父類的構造方法Animal(String name),定義如下:
Animal(String name){
this.name=name;
}
因此會把傳過來的字符串“Catname”傳遞給父類對象的name屬性。當Cat(String n,String c)構造方法調用結束后,真真正正在堆內存里面new出了一只Cat,這只Cat里面包含有父類對象Animal,這個Animal對象有自己的屬性name,name屬性的值為調用父類構造方法時傳遞過來的字符串Catname。除此之外,這只Cat還有自己的私有成員變量eyesColor,eyesColor屬性的屬性值為調用子類構造方法時傳遞過來的字符串blue。所以執行完這句話以后,內存中的布局是棧內存里面有一個引用c,c指向堆內存里面new出來的一只Cat,而這只Cat對象里面又包含有父類對象Animal,Animal對象有自己的屬性name,屬性值為Catname,Cat除了擁有從Animal類繼承下來的name屬性外,還擁有一個自己私有的屬性eyesColor,屬性值為blue。這就是執行完第一句話以后整個內存布局的情況如下圖所示:
接着看這句話:Lady l1 = new Lady(“l1”,c);
程序執行到這里,首先在棧內存里面多了一個引用變量l1,l1里面裝着一個值,通過這個值可以找到在堆內存里面new出來的Lady對象。l1就是這個Lady對象的引用,l1指向Lady對象。在創建Lady對象時,調用Lady類的構造方法:Lady(String name,Animal pet),其定義如下:
Lady(String name,Animal pet){
this.name=name;
this.pet=pet;
}
這個構造方法有兩個參數,分別是String類型的name和Animal類型的pet,pet參數是一個父類對象的引用類型,這里把l1和c作為實參傳遞給了構造方法,接着在構造方法里面執行this.name=name,把傳遞過來的l1由傳給Lady對象的name屬性,因此Lady對象的name屬性值為l1,這里也把前面new出來的那只Cat的引用c傳遞給了構造方法里面的參數pet,接着在構造方法里面執行this.pet=pet,pet參數又把c傳過來的內容傳遞給Lady對象的pet屬性,因此pet屬性的屬性值就是可以找到Cat對象的地址,因此Lady對象的pet屬性也成為了Cat對象的引用對象了,通過pet里面裝着的值是可以找到Cat對象的,因此pet也指向了Cat,但並不是全部指向Cat,pet指向的只是位於Cat對象內部的Animal對象,這是因為在調用構造方法時,是把c當成一個Animal對象的引用傳過來的,把c作為一個Animal對象傳遞給了pet,所以得到的pet也是一個Animal對象的引用,因此這個pet引用指向的只能是位於Cat對象里面的Animal對象。在我pet引用對象眼里,你Cat對象就是一只普通的Animal,訪問你的時候只能訪問得到你里面的name屬性,而你的eyesColor屬性我是訪問不到的,我能訪問到你的name屬性,訪問的是位於你內部里面的父對象的name屬性,因為我pet引用本身就是一個父類對象的引用,因此我可以訪問父類對象的全部屬性,而你子類對象Cat自己新增加的成員我pet引用是訪問不了的。不過現在我pet引用不去訪問你父類對象的成員變量name了,而是去訪問你的成員方法enjoy了。首先是使用Lady對象的引用l1去調用Lady對象的myPetEnjoy()方法,myPetEnjoy()方法定義如下:
public void myPetEnjoy(){
pet.enjoy();
}
然后在myPetEnjoy()方法體里面又使用pet引用對象去調用父類對象里面的enjoy方法。
方法是放在代碼區(code seg)里面的,里面的方法就是一句句代碼。因此當使用pet引用去訪問父類對象的方法時,首先是找到這個父類對象,然后看看它里面的方法到底在哪里存着,找到那個方法再去執行。這里頭就比較有意思了,code seg里面有很多個enjoy方法,有父類的enjoy()方法,也有子類重寫了從父類繼續下來的enjoy()方法,那么調用的時候到底調用的是哪一個呢?是根據誰來確定呢?注意:這是根據你實際當中的對象來確定的,你實際當中new出來的是誰,就調用誰的enjoy方法,當你找這個方法的時候,通過pet引用能找得到這個方法,但調用代碼區里面的哪一個enjoy方法不是通過引用類型來確定的,如果是通過引用類型pet來確定,那么調用的肯定是Animal的enjoy()方法,可是現在是根據實際的類型來確定,我們的程序運行以后才在堆內存里面創建出一只Cat,然后根據你實際當中new出來的類型來判斷我到底應該調用哪一個enjoy()方法。如果是根據實際類型,那么調用的就應該是Cat的enjoy()方法。如果是根據引用類型,那么調用的就應該是Animal的enjoy()方法。現在動態綁定這種機制指的是實際當中new的是什么類型,就調用誰的enjoy方法。所以說雖然你是根據我父類里面的enjoy方法來調用,可是實際當中卻是你new的是誰調用的就是誰的enjoy()方法。即實際當中調用的卻是子類里面重寫后的那個enjoy方法。當然,講一點更深的機制,你實際當中找這個enjoy方法的時候,在父類對象的內部有一個enjoy方法的指針,指針指向代碼區里面父類的Animal的enjoy方法,只不過當你new這個對象的時候,這個指針隨之改變,你new的是什么對象,這個指針就指向這個對象重寫后的那個enjoy方法,所以這就叫做動態綁定。只有在動起來的時候,也就是在程序運行期間,new出了這個對象了以后你才能確定到底要調用哪一個方法。我實際當中的地址才會綁定到相應的方法的地址上面,所以叫動態綁定。調這個方法的時候,只要你這個方法重寫了,實際當中調哪一個,要看你實際當中new的是哪個對象,這就叫多態,也叫動態綁定。動態綁定帶來莫大的好處是使程序的可擴展性達到了最好,我們原來做這個可擴展性的時候,首先都是要在方法里面判斷一下這只動物是哪一類里面的動物,通過if (object instanceof class)這樣的條件來判斷這個new出來的對象到底是屬於哪一個類里面的,如果是一只貓,就調用貓的enjoy方法,如果是一條狗,就調用狗的enjoy方法。如果我現在增加了一個Bird類,那么擴展的時候,你又得在方法里面寫判斷這只鳥屬於哪一個類然后才能調用這只鳥的enjoy方法。每增加一個對象,你都要在方法里面增加一段判斷這個對象到底屬於哪個類里面的代碼然后才能執行這個對象相應的方法。即每增加一個新的對象,都要改變方法里面的處理代碼,而現在,你不需要再改變方法里面的處理代碼了,因為有了動態綁定。你要增加哪一個對象,你實際當中把這個對象new出來就完了,不再用去修改對象的處理方法里面的代碼了。也就是當你實際當中要增加別的東西的時候,很簡單,你直接加上去就成了,不用去改原來的結構,你要在你們家大樓的旁邊蓋一個廚房,很簡單,直接在旁邊一蓋就行了,大樓的主要支柱什么的你都不用動,這就可以讓可擴展性達到了極致,這就為將來的可擴展打下了基礎,也只有動態綁定(多態)這種機制能幫助我們做到這一點——讓程序的可擴展性達到極致。因此動態綁定是面向對象的核心,如果沒有動態綁定,那么面向對象絕對不可能發展得像現在這么流行,所以動態綁定是面向對象核心中的核心。
總結動態綁定(多態):動態綁定是指在“執行期間”(而非編譯期間)判斷所引用的實際對象類型,根據其實際的類型調用其相應的方法。所以實際當中找要調用的方法時是動態的去找的,new的是誰就找誰的方法,這就叫動態綁定。動態綁定幫助我們的程序的可擴展性達到了極致。
多態的存在有三個必要的條件:
- 要有繼承(兩個類之間存在繼承關系,子類繼承父類)
- 要有重寫(在子類里面重寫從父類繼承下來的方法)
- 父類引用指向子類對象
這三個條件一旦滿足,當你調用父類里面被重寫的方法的時候,實際當中new的是哪個子類對象,就調用子類對象的方法(這個方法是從父類繼承下來后重寫后的方法)。
面向對象比較強調類和類之間,對象和對象之間的一種組織關系,如果能把這種組織關系組織得比較好的話,你的程序想擴展性比較好,比較健壯,維護性比較好這些都可以達到,關鍵看你的設計到底好還是不好。