小孩子多大適合學程式設計?
一、介紹
面嚮物件語言的三大特性:封裝、繼承、多型,而在這三種特性中,多型又是那個極具意義的一個,從某個方面來說,一個OOP語言最核心的特徵就是多型,封裝繼承在很多方面都是為了實現多型而出現的。
而多型又可以分為兩種:
1、編譯時多型(靜態多型):在編譯的時候就知道要呼叫的方法,如過載
2、執行時多型(動態多型,又稱動態繫結):在執行的時候才知道要呼叫哪個方法
過載是在我們單獨拎出來的一個定義,而我們常說的多型一般指執行時的多型,也就是說我們要等最後執行的時候才會確定要呼叫的方法,這與Java虛擬機器的執行機制有關,所以多型方法又稱延遲方法。
二、多型的實現方法
多型的實現方法有兩種:
1、子類繼承父類:子類繼承父類並重寫了父類的方法
2、實現介面方法:實現介面的類實現了介面的方法
其實這兩種方法有共同點:就是實現了父類或介面的方法,而多型最核心的就是這些被重寫或者被實現方法在子類中的不同表現形式。
向上轉型:
講多型的實現方法又必須知道的一個概念:向上轉型 看一個例子:
1 class ceshi{ 2 public static void main(String[] args) { 3 animal a1 = new animal(); 4 animal a2 = new Dog(); 5 animal a3 = newCat(); 6 a1.speak(); 7 a2.speak(); 8 a3.speak(); 9 } 10 } 11 class animal{ 12 public void speak() { 13 System.out.println("this is an animal"); 14 } 15 } 16 17 class Dog extends animal{ 18 @Override 19 public void speak() { 20 System.out.println("this is a Dog");21 } 22 } 23 class Cat extends animal{ 24 @Override 25 public void speak() { 26 System.out.println("this is a Cat"); 27 } 28 }
這裡我們有一個基類animal,Dog類和Cat類繼承了animal類並且Override其speak方法,這裡我們可以稱Dog和Cat是animal的子類(匯出類),注意此時的mian函式的三個定義方法,第一個是正常的new了一個animal物件,而第4行和第5行就是向上轉型,可以思考一下輸出是什麼
this is an animal this is a Dog this is a CatView Code
神奇的事情發生了,我們的引用變數明明定義的animal類,而實際執行的方法卻是對應子類的方法,這樣JVM的機制有關,具體可以見這篇文章:
https://www.cnblogs.com/kaleidoscope/p/9790766.html
而這就是我們所要介紹的向上轉型,左邊定義的基類的類別,而實際執行的是右邊子類的方法,向上轉型就是這樣一個特點
對於:
animal a2 = new Dog();
左邊是編譯型別,其編譯型別為animal,就是告訴編譯器我定義了一個annimal型別的引用變數,我將要指向一個物件
右邊是執行型別,其執行型別是Dog,這就是我們上面說的動態繫結機制,只在真正執行的時候去找具體實現的方法,而現在找到了執行型別是Dog,所以執行Dog.speak
那再總結一下發生了什麼:
1、Dog繼承了animal
2、Dog重寫了animal的speak方法
3、在main函式裡出現了向上轉型(也就是一個基類的引用指向了一個子類的物件)
這樣幾個條件,多型發生了,也正是多型機制使一個基類的引用指向了一個子類的物件的方式讓編譯器承認了
同樣這裡需要注意第二個Dog重寫了speak方法,那萬一speak是靜態方法呢?會出現什麼情況?
如果你去思考執行結果就掉入了思維陷阱,靜態方法是不能被重寫了,靜態代表它只有一份,不允許除它之外的物件去重寫,所以speak是靜態方法,嘗試重寫一定會報錯。
那再思考一個問題:如果沒有重寫方法會怎麼樣?看如下程式碼:
1 class ceshi{ 2 public static void main(String[] args) { 3 animal a1 = new animal(); 4 animal a2 = new Dog(); 5 animal a3 = new Cat(); 6 a1.speak(); 7 a2.speak(); 8 a3.speak(); 9 } 10 } 11 class animal{ 12 public void speak() { 13 System.out.println("this is an animal"); 14 } 15 } 16 17 class Dog extends animal{ 18 19 } 20 class Cat extends animal{ 21 22 }
可以看到,我們把子類裡面的speak方法都刪除了,這就與繼承的特性相關了,思考一下結果是什麼?
this is an animal this is an animal this is an animalView Code
這裡發生的事情就是,我們的執行型別是Dog或者Cat,執行時首先去這兩個子類裡去找speak方法,如果發現找不到,就會向上去找,到父類是animal去找,發現找到了就會直接執行,這個向上的過程是可以無限向上了,直到找到或者到Object類才會結束。
那我們再思考一個問題,我們前面討論的問題中子類呼叫的是重寫父類的方法,那向上轉型後的引用變數能呼叫子類特有的方法麼?如:
1 class ceshi{ 2 public static void main(String[] args) { 3 animal a2 = new Dog(); 4 animal a3 = new Cat(); 5 a2.eat();//出錯 6 a3.eat();//出錯 7 } 8 } 9 class animal{ 10 public void speak() { 11 System.out.println("this is an animal"); 12 } 13 } 14 15 class Dog extends animal{ 16 public void eat(){ 17 System.out.println("Dog eat"); 18 } 19 20 } 21 class Cat extends animal{ 22 public void eat(){ 23 System.out.println("Cat eat"); 24 } 25 }
我們可以看到我們用向上轉型的引用變數是無法呼叫Dog和Cat類特有的方法eat,也就是第五行和第六行會報錯,我們看一下它錯誤的原因:
我們驚訝的發現,編譯器說無法到animal中找到eat方法,我們前面說過,對於一個:
animal a2 = new Dog();
這樣定義的引用變數a2,其編譯型別是左邊的animal,其執行型別是右邊的Dog,從上面的結果來看,編譯器先確定了我們總共的方法有哪些,就是從編譯型別中確定了方法的多少,也就是執行型別中特有的方法編譯器是找不到的。
那我們可以下一個結論:向上轉型的引用變數能呼叫的方法的數量(種類)是由編譯型別(左邊的)決定的,而到具體執行,先看子類有沒有重寫,如果重寫就呼叫子類,沒有就往上追溯,也就是實際方法的實現呼叫順序是從右邊的執行型別開始,找到就呼叫,沒找到就往上(父類)去找這個方法。
這就是向上轉型的全貌,那可能想問?這有什麼意義麼?我引用它處的一段,真正還得自己使用了去體會,如下:
1.可替換性(substitutability)。多型對已存在程式碼具有可替換性。例如,多型對圓Circle類工作,對其他任何圓形幾何體,如圓環,也同樣工作。
2.可擴充性(extensibility)。多型對程式碼具有可擴充性。增加新的子類不影響已存在類的多型性、繼承性,以及其他特性的執行和操作。實際上新加子類更容易獲得多型功能。例如,在實現了圓錐、半圓錐以及半球體的多型基礎上,很容易增添球體類的多型性。
3.介面性(interface-ability)。多型是超類通過方法簽名,向子類提供了一個共同介面,由子類來完善或者覆蓋它而實現的。如圖8.3
所示。圖中超類Shape規定了兩個實現多型的介面方法,computeArea()以及computeVolume()。子類,如Circle和Sphere為了實現多型,完善或者覆蓋這兩個介面方法。
4.靈活性(flexibility)。它在應用中體現了靈活多樣的操作,提高了使用效率。
5.簡化性(simplicity)。多型簡化對應用軟體的程式碼編寫和修改過程,尤其在處理大量物件的運算和操作時,這個特點尤為突出和重要。
三、向下轉型
上面說完了向上轉型,其中的一個特點就是引用變數能呼叫方法的種類是由編譯器決定的,我們無法呼叫子類特有的方法,那當我們真的想呼叫子類中特有的方法都時候,就可以採用向下轉型的方式,如:
1 class ceshi{ 2 public static void main(String[] args) { 3 animal a2 = new Dog(); 4 animal a3 = new Cat(); 5 Dog a4 = (Dog) a2;//向下轉型 6 a4.eat();//呼叫 7 a2 = (animal)a4;//轉回animal 8 a2.speak(); 9 } 10 } 11 class animal{ 12 public void speak() { 13 System.out.println("this is an animal"); 14 } 15 } 16 17 class Dog extends animal{ 18 public void speak() { 19 System.out.println("this is an Dog"); 20 } 21 public void eat(){ 22 System.out.println("Dog eat"); 23 } 24 25 } 26 class Cat extends animal{ 27 public void eat(){ 28 System.out.println("Cat eat"); 29 } 30 }
結果:
Dog eat
this is an Dog
觀察第四行和第五行就可以發現,我們將a2的編譯型別轉成了Dog並賦給了a4,a4成功呼叫了Dog的獨有方法eat(),而a4呼叫完eat後,同樣也可以將編譯型別重轉為animal,這樣a4就是一個向上轉型後的引用變數,編譯型別變成了animal,執行型別還是Dog。
可以觀察出一個規律,我們用型別轉換的時候只能修改它的編譯型別,而執行型別早在定義的時候就已經固定了(本質上是地址已經固定)。
11