# 物件導向的三大特性 : 封裝,繼承,多型 物件導向的三大特性:封裝、繼承、多型 A.封裝性(Encapsulation): 封裝(Encapsulation)的概念就是在程式碼中設置權限,讓不同的物件之間有不同的存取限制,而不是把所有資料都攤在陽光下讓大家使用。「封裝」可防止程式的原始碼被竄改,保障了資料的隱密性,並提高了程式的穩定性和安全性,你總不可能希望辛苦做好發佈出去的程式,可以被使用者隨便亂改吧? 封裝的程式碼實作是透過「存/取控制修飾子」來達成的,不同的程式語言會有更細緻的分類與不同的使用方法,但我們這裡只介紹最常用的三種:public、private和protected。 封裝性的「存/取控制修飾子」: 「存/取」是兩個動作,也就是儲存和取用,但我們一般把它當作「使用」的意思就好,並直接合併在一起稱呼它為「存取」。 public、private、default與protected等存取控制修飾子,可以於宣告類別(class)、建構子(constructor,或翻譯作建構子)、介面(interface)與函式(或翻譯作函數、方法)時使用。 封裝性的各種修飾子也可以應用在方法(成員函數)上 * 註:修飾子,可想成是英文的形容詞,加在名詞或動詞的前面,例如「私人的錢包」,這個「私人的」就是修飾子。 public: public的英文是「公開的、公眾的」,意思最容易了解,就是不做任何管控限制。比喻來說,就像路邊的飲料販賣機,任何人都可以去買,以程式來說就是把所有的資料都攤在陽光下讓大家共享。 被宣告為public的成員變數或成員函式,可以讓同一類別或物件的成員存取使用,也可以讓其他類別或物件的成員存取使用。 private: 被宣告為private的成員變數或成員函數,只能讓同一個類別或物件的成員存取使用,禁止讓其他類別或物件的成員存取使用。 private的英文是「私人的、私有的」,就是像私房錢一樣的意思。例如我們由Student類別創造了一個「小明」的物件,並且在小明的物件中實作了一些private方法,則這些private方法只能給小明這個物件使用,語法為「小明 .方法名稱();」。若今天阿輝要來使用這個方法,若寫作:阿輝 .方法名稱();,即使語法正確,complier還是會出現錯誤訊息,告知使用者這個方法受到private修飾子的管控,所以阿輝不能使用。 不過因為private的限制條件太嚴格了,若把類別或物件的成員變數與成員函式全部都宣告為private,那這個物件就等於搞自閉,完全不能與別的物件互動,這樣就失去物件導向的精神了。所以,即使是被宣告為private的成員變數,也能透過public方法做存取,與別的物件互動。 什麼意思呢?舉個例子,例如製作RPG遊戲時,我們可以把「劍士」物件的成員變數atk(攻擊力)宣告為private,再宣告一個public方法來執行攻擊,例如叫它attack()方法。attack()方法的定義會使用到atk這個private變數來做計算,例如:傷害值 = atk數值 * 裝備加成 * 命中率。 這樣我們就達到目的:即我們既可以防止使用者隨意亂改劍士的atk數值,也可以透過attack()這個public方法來存取atk這個private變數,和其他物件做互動,例如造成了「暗黑龍」這個物件350點的傷害。 protected: protected英文的意思是「受保護的」,被宣告為protected的成員變數或成員函數,只能讓同一個類別或物件的成員、以及「繼承於它」的子物件存取使用,禁止讓其他物件的成員存取使用。 以上面private的例子來說,若改為protected修飾子,則就是除了小明,以及繼承於小明的子類別(例如小明的兒子、孫子…)可以使用protected方法,其他「外人」還是不能使用。除了這點不同之外,其餘protected的觀念和用法,都和private相同。 總結一下上面所講的東西,並且再把套件(package)功能也一併放進來考慮: 1. public:用public修飾的類別、屬性及方法,代表不設任何權限,即使位於不同套件(資料夾階層的概念)的類別,無論是不是繼承來的子類別,都能存取使用,反正就是無論怎樣都可以的意思。 2. protected:用protected修飾的類別、屬性及方法,基本上和public都相同,權限都放的很開。不過,對於不同套件的類別,除非有繼承關係才可以存取使用。換句話說,若沒有繼承關係,不同套件的類別就無法存取使用。 3. default:當不寫任何存取控制修飾子的時候,預設就是default修飾子,所以實際上並不會真的把default寫出來。基本上和protected都相同,不過有一點比protected嚴格,就是不論有無繼承關係,只要是不同套件(資料夾階層的概念)的類別,都無法存取使用。 4. private:用private修飾的類別、屬性及方法,只有在同一個類別(class)的 { } 區塊內的成員才能存取它,其餘都免談。如果其他類別或物件想要存取private修飾的東西,可以透過該類別或物件內的public方法來達成。 所以,四個JAVA存取控制修飾子,權限的管控強度由低到高分別為: public  protected  不寫(default) private * 註:注意若不寫存取控制修飾子,在不同的程式語言有不同的意義,例如C++或C# 的class中若不寫存取控制修飾子,所代表的是private而不是JAVA的default,因此必須確認不同程式語言的情況。 B.繼承性(Inheritance): 繼承性(Inheritance)的概念很簡單,可用日常生活的比喻來理解。例如,兒子繼承了爸爸的家業(子類別會繼承父類別的屬性和方法),所以兒子會有父親已經做過的東西,而不必再重新做一次。而在程式語言中,繼承最大的好處是可以不必一再撰寫重複的程式碼,不只節省心力和時間,更重要的是可以提高程式的可讀性,增加程式的結構化程度,並讓維護和新增功能時更加容易方便、減少錯誤。 除了上一課提到,我們可以透過繼承把多個 class 重複的程式碼寫在同一個 class 以減少「Class 之間」的重複程式碼之外,我們也可以利用繼承減少「class 內部」的重複程式碼 如果我們想讓 Monster 可以攻擊不同的生物,像是村民或其他怪物,那可能需要為不同的生物撰寫不同版本的 Attack: ``` public void Attack(Villager villager) { .... } public void Attack(Monster monster) { .... } ``` 但是我們都知道 Villager 與 Monster 就繼承自 Creature,所以我們可以這樣寫: ``` public void Attack(Creature creature) { .... } ``` 這樣只要是 Creature 或是它的衍生類別,都可以被傳入 Attack 處理,就可以大大減少 Class 內部的重複程式碼。 為什麼我們要這麼熱衷於減少重複的程式碼?因為重複的程式碼會讓程式變得又臭又長,不易閱讀、不易除錯,降低程式的可維護性,更會占用額外的記憶體空間,有害無利。 繼續這個比喻,兒子不但繼承了父親所有的程式碼,同時兒子也可以在這個基礎上再增加新的程式碼,所以子類別的成員就是:「父類別的成員加上自己的(新)成員」。或者,子類別也可以修改父類別所留下來的東西(即「複寫,override」)。所以理論上來說,子類別會比它的父類別有更高的靈活性與自由度,並且更加多元,即「青出於藍勝於藍」的概念。 父類別如爸爸,子類別如兒子,爸爸有的,兒子繼承了也會有,還可以加上兒子自己才有的東西,青出於藍勝於藍。而應用在程式撰寫,繼承的東西當然就是程式碼,所以繼承的好處是可以減少重覆撰寫相同的程式碼,有程式碼再利用的概念。 和生物的家族觀念一樣,一個小孩只會有一個爸爸,但一個爸爸卻可以有很多個小孩。程式碼也是一樣的,每個類別只能有一個父類別,而父類別可擁有一個以上的子類別。 父類別和子類別的概念是相對的,也就是子類別可以再衍生出更多子類別,這時候的子類別就可看成是父類別,而原本的父類別就變成「祖父」類別 … 和人類家族觀念的父親、祖父、兒子、孫子、曾孫…觀念完全一樣,很容易了解,我們稱為「多層」繼承。注意,「多層」繼承和「多重」繼承雖然中文只差一個字,但卻是完全不同的觀念喔。 剛才講到多層繼承,現在來說明一下它和多重繼承的不同。當一個子類別是從多個父類別繼承而來時,我們稱之為「多重繼承」,例如兒子並不只是繼承了父親的家業,也同時繼承了叔叔、伯伯 … 的家業,是一種平行的關係,和多層繼承的垂直關係不同。不過,和人類在分家產的時候一樣,多重繼承在實際撰寫程式時,很容易產生邏輯混亂、出現糾紛,使用方式也較為困難,因此有些程式語言(如Java)就內定不支援多重繼承(除非以比較間接的方式實現,例如實作多個介面),因此建議若非經驗豐富的設計者,盡可能少使用多重繼承。 補充:不同的程式語言有不同的稱呼,Java中習慣稱為父類別,C# 習慣稱為基底(base)類別,或者你也可以看到基礎類別、父類別 … 等不同名稱,但說的都是同一個東西。至於子類別也有人稱為衍生類別(derived class),知道一下還有這些稱呼即可。 最後再說到很重要的東西,就是子類別無法繼承到父類別的「private成員」(如果有的話)、「建構子」(一定有,但無法被繼承),以及被宣告為「final」的成員(如果有的話)。也就是說,不是任何東西都能從父類別繼承給子類別,這三個東西是不能被繼承的,如下圖所示: C.多型性(Polymorphism): 多型性(Polymorphism)的概念,又可分為多載(Overloading)和複寫(Overriding),以下分成兩個子項目來解說。 一、多載(overloading): 多載的概念簡單來說,就是相同名稱的方法(Method),藉由傳給它不同的參數(函數的輸入值),它就會執行不同的敘述,以產生不同的輸出。就像是同一台果汁機,丟進去蘿蔔就會輸出蘿蔔汁,丟進去蘋果就會輸出蘋果汁。而且,由於蘋果比較硬,所以這台優秀的果汁機會自動把刀片旋轉的力道和轉速調強一點。 以上比喻,用程式語言的術語表達就是:「同一個方法(Method)會依據它的參數值(輸入值)的「型態」、「數量」,甚至「順序」的不同,自動選擇對應的定義,執行不同的敘述,輸出不同的結果」。 以下面這段計算矩形面積的程式碼來說,有兩個完全同名的函數(都叫computeArea()),但函數的參數定義不同,讓編譯器自動判斷:依據現在輸入的參數,要呼叫此函數的哪一個版本,這就叫做多載(overloading)。當你寫「computerArea(8);」,編譯器就會呼叫上面那個定義,當作正方形來計算面積,輸出64。而當你寫「computerArea(8, 5);」,編譯器就會呼叫下面那個定義,當作長方形來計算面積,輸出40。 以這個計算面積的例子來說,還可以定義同一個函數的參數「型態」為不同,讓編譯器自動執行多載的判斷。例如,我可以在上面的程式碼中,再加入這幾行: ``` public double computeArea(double length, double width) { return length * width ; } ``` 這樣也一併包含了長、寬不是剛好為整數,而是有小數點的場合。 多載(Overloaded): 如果今天有兩個method,這兩個method的名稱相同,但是signatures不同,那就是 overloaded。 多載:同名異式 只有方法的名稱相同,而只要方法的回傳值型態、參數的型態、參數的數量 … 有一個不同,就是多型,而不是多載。 那甚麼是 signature 呢?signatures 指的就是引數(輸入值)的種類、數目、排列與 method 名稱,例如下面這兩個 method 他們的變數名稱雖然不同,但是因為種類相同,method 名稱也相同,所以 signatures 就算相同。 並不是只有method才有overloaded,建構子也可以做多載。 Example 1: void sleep(int time, string s) {....}void sleep(int sleepTime, string sound) {....} 而下面這兩個雖然引數數目相同,但是型態不同,所以 signatures 就不相同 Example 2: void eat(int numberOfFood) {....}void eat(string foodName) {....}因此我們只需要辨別 signatures ,就可以知道 method 是否有重複。重複的 method 是沒有意義的,因為這種情況下,程式無法得知你到底是要呼叫哪一個 method。如 Ex1 中,C# 會當作沒看到第二個 method,所以你永遠無法呼叫到它。那你又為何要這樣寫呢? 另外一個常見問題是,引數與 method 名稱都相同,但是 output 型態不同算是重複嗎? 答案是,算!請看下面這個例子 Example 3: int run() {...}void run() {...}如果我今天呼叫 run() 的話,你可以辨別我是要呼叫哪一個 run() 嗎?相信你應該了解了吧?這就是為什麼這樣也算是重複的 method。 多型: 我們說要出去打球,但如果拿的是不同的球,就是打不一樣的球。程式語言也是一樣,雖然都是打球的playball()方法,但如果給這個方法傳入不同的球,就會執行不同的操作。例如: playball(棒球); // 打棒球 playball(保齡球); // 打保齡球 playball(羽毛球); // 打羽毛球 當然,程式設計師必須要先定義好這三個不同球種的實際程式碼,但無論如何,我們可以使用同一個方法名稱playball(),而不需要去額外定義三個不同名稱的方法,例如不需要再去定義:playbaseball()、playbowling()、playbadminton()三個連名稱都不同的方法。 這樣做的好處是可以增加程式的可讀性和結構化的程度,就像我們可以把洗衣和烘衣的程式碼定義在同一台全功能洗衣機,而不用分別去買洗衣機和烘衣機。這種觀念稱為多型,為「同名異式」的實際應用。 ``` baseball.play(); bowling.play(); badminton.play(); ``` 以上這樣也是多型的一種:不同的物件可以使用同名的方法,但方法的實際內容並不一樣(因為scope不同 … 因此沒有名稱衝突的問題)。 因為同一個類別可以分別建立多個物件,每一個物件都可以呼叫自己的方法,或存取自己的屬性,所謂執行物件的方法就是針對不同物件送出不同的訊息,如下所示: ``` now.printTime(); open.printTime(); close.printTime(); ``` 雖然三個物件(now、open、close)都是執行printTime()方法,但primtTime()方法的實際內容依據不同物件而有所不同。 JAVA的多載還有一種變體,就是可以不必事先指定參數的數量,術語稱作「可變(數量)參數」,又稱「不定個數參數」,可以支援「任意數量的輸入值」當作函數的參數。 舉個例子,我們可以定義一個函數add(),這個函數會把所有輸入的整數值都相加起來,並回傳計算後的結果。假如在沒有多載功能的C語言中,我們會這麼寫:「int add(int x, int y){ return x + y;}」,不過這個函數只能適用於2個輸入值的情況,若是要計算3個數或更多個數的相加就無能為力了,除非我們事先知道到底有幾個數要相加,但使用起來就很沒有彈性。 而使用JAVA的可變(數量)參數功能,則無論我們有多少個輸入值,add()函數都來者不拒,能幫我們計算出總和,非常好用。 要使用可變(數量)參數,實際的程式碼如下: ``` int[ ] arr = { a, b, c, d, e, f }; // 宣告一個陣列物件,其中a ~ f 是任意數量的整數 class Addnumber { // 定義一個類別,其名稱為Addnumber int add(int… arr) { // 在函數的()區塊內,寫上「型態 … 物件(或變數)名稱」 int sum = 0 ; for(int i = 0; i < arr.length ; i++){ // 定義一個for迴圈 sum = sum + a[i] ; // 計算陣列arr中所有元素的總和 } return sum ; // 回傳add()函數的計算結果 } } ``` 以上程式碼,無論arr陣列中的元素有幾個,add()函數都能正確輸出總和,例如我們輸入「add(2,3);」就會得到sum的值為5,輸入「add(1,3,5,8,7,6,4,2);」就會得到sum的值為36,使用上非常靈活。 如果在不是物件導向、沒有多載功能的C語言,對於相同名稱的函數來說,只要函數的參數(輸入值)的型態、數量或順序不同,則C語言就會判定它們是不同的函數,所以使用者必須要定義數個不同名稱的函數,也就是說要購買蘿蔔專用的蘿蔔果汁機、蘋果專用的蘋果果汁機 … 非常麻煩,所以多載功能的優勢就顯現出來了。 二、複寫(Overriding): 上面計算矩形面積的例子已經示範過何謂「多載」,而複寫(Overriding)是指子類別對其父類別的方法(method)做改寫、並取而代之。複寫的觀念也很容易理解,打個比方,兒子繼承了父親的公司,並且對公司制度做了多項改革,例如行政流程全面電腦化、導入ERP系統管理庫存、汰換過時的設備、甚至於人事變動…等,這就是複寫。 以實際的程式碼來看。以下的程式碼中,動物類別中定義了凡是Animal類別都有4隻腳,並且定義了getLegs()方法可以回傳有4隻腳的資訊。而鳥(Bird)類別繼承了Animal類別,但因為鳥只有2隻腳,所以讓Bird類別將Animal類別的getLegs()方法給覆寫掉,改成回傳2隻腳。 <-- 這是C# 如果子類別要對父類別的(同一個)方法做複寫,我們會在子類別中複寫的(新)方法前面加上「@Override」的「JAVA註解」作為標記。如果不加的話程式雖然還是可以執行,但如果在子類別複寫時打錯字,編譯器也不會告訴我們哪邊錯了,加上考慮到程式的可讀性,建議不要偷懶,一律都把它寫上去。 我們把「@Override」的術語稱為「JAVA註解」,英文不是comment而是「annotation」。意思就是說,這個註解不只給人看,也是給編譯器看的,這樣編譯器才知道這個是要複寫的(新)方法。話雖這麼說,但其實就把「JAVA註解」當作是一般的關鍵字來使用就好。  註:除了@Override,其他的「JAVA註解」還有: 1. @Deprecated:不建議使用的方法,意思就是,JAVA的編譯器不會優先去使用這個方法。 2. @SuppressWarnings:加上這個JAVA註解的程式碼段落,將不會出現編譯器警告,也就是即使這 個段落有地方出錯了,編譯器也不會告訴你。通常是用在自己對這些錯誤已經心知肚明,但為 了某些原因,不想讓編譯器主動提醒(煩)你。 「複寫」同時也可以搭配「繼承」和「多載」來使用,例如在子類別中,以多載的方式,複寫父類別中的方法(method)。 注意,複寫不能與多型搭配使用,也就是說要複寫的新方法,其方法名稱和參數的類型、數量、順序都要和被複寫的原方法相同才行。  註:在C# 語言中,是在要「被複寫」的方法前面加上virtual關鍵字,而在「要複寫的新方法」前面加上override關鍵字,和Java是用@override的語法不同。 ----------------------------------------------------------------------------------------- 多型 多型,指同一個行為,但有不同的結果,例如滑鼠左鍵點擊,有時點擊是確認事件,在遊戲就可能是射擊事件,但同樣都是滑鼠左鍵點擊,卻執行不一樣的內容,如此一來,可以讓我們更有彈性的設計,不會被侷限只能永遠特定型別才能被呼叫使用,而是可以不斷延伸擴展出來更多種型別。 Rhino的指令 – 滑鼠左鍵或右鍵有不同的功能,也是多型。 範例 ``` class Animal { void move() { System.out.println("父類別 move ... "); } } class Dog extends Animal { void move() { System.out.println("小狗陸地跑..."); } } class Bird extends Animal { void move() { System.out.println("小鳥天上飛..."); } } class Fish extends Animal { void move() { System.out.println("小魚水中游..."); } } class HKTDemoJava { public static void main(String[] args) { Animal animal = new Animal(); Dog dog = new Dog(); Bird bird = new Bird(); Fish fish = new Fish(); moveAnimal(animal); moveAnimal(dog); moveAnimal(bird); moveAnimal(fish); } static void moveAnimal(Animal animal) { animal.move(); } } 執行結果: 父類別 move ... 小狗陸地跑... 小鳥天上飛... 小魚水中游... ``` 方法重載(Overload)和方法覆寫(Override)差異比較 • 方法重載(Overload): 我們在方法那一節討論到,方法重載(Overload),即在一個類別當中,有同樣的方法名稱,但參數資料不同。 • 方法覆寫(Override)): 子類別繼承父類別,覆寫父類別方法,方法名稱與參數都一樣。子類別可以根據自己需求重新定義改寫方法。 ________________________________________ 抽象 抽象,abstract,為一個修飾子,可以用來修飾類別和方法。可以把他想像成是一種合約的概念,當你繼承抽象類別,你就必須遵守這份繼承合約,繼承者需在子類別中實作這些父類別所交代要定義的方法。 抽象修飾類別語法 注意的是,抽象類別無法被實體化,只能被繼承。所以也不能定義 private 的抽象類別。 ``` abstract class 類別名稱{ //todo sth... } ``` 範例 ``` abstract class AbstractFather(){ void makeMoney(){ System.out.println("makeMoney ... "); } } ``` 抽象修飾方法語法 同理,抽象方法無法被實體化後使用也不能定義 private 的方法,只能被覆寫且方法大括號內不能有任何程式。另外,抽象方法只能被定義在抽象類別中,否則一樣會編譯報錯。 abstract 封裝修飾子 回傳型態 方法名稱(<參數...>); //抽象方法不能定義方法本體,簡單說就是這裏不能有任何程式,否則會編譯報錯。 範例 `abstract public void makeMoney();` 介面 介面語法 封裝修飾子 interface 介面名稱{ //方法 } 範例 ``` public interface Info{ void getInfo(); } ``` 類別實作介面 當類別要使用此介面,將會使用 implements 這個關鍵字,介面裡面宣告的方法都並需時做出來,否則會報錯。 ``` class People implements Info{ public void getInfo(){ System.out.println("獲取資訊") } } ``` 類別實作多個介面 語法 ``` class 類別名稱 implements 介面1,介面2,...介面x{ // } ``` 範例 ``` interface A { void getAInfoData(); } interface B { void getBInfoData(); } class HKTDemoJava implements A, B { public static void main(String[] args) { } @Override public void getAInfoData() { } @Override public void getBInfoData() { } } ``` 介面繼承介面 ``` public interface 介面名稱 extends 介面1, 介面2 { // ... } ``` 範例 ``` interface A { void getAData(); } interface B extends A { void getBData(); } public class Main implements B { public static void main(String[] args) { } @Override public void getBData() { } @Override public void getAData() { } } ``` 抽象類別與介面比較 抽象類別 • 一個類別只能繼承一個抽象類別 • 抽象類別可以定義方法本體,簡單雛形程式 • 以資料為主體 • 資料欄位,事後可修改變更資料值 介面 • 一個類別可以實作多個介面 • 介面不能定義方法本體 • 以行為方法為主體 • 資料欄位,預設是 public static final,所以資料事後不能修改 從零開始學 Java 程式設計】繼承 作者: HKT - 4月 21, 2019 繼承 繼承,在人類的世界當中簡單的定義是父親或家族之前打拼下來的江山,傳給下一代。下一代的兒女、子孫,將繼承擁有上一代的財產或事業。在程式當中可以定義一個子類別透過關鍵字 extends 來繼承父類別所定義屬性欄位、方法等功能。如果父類別有不符合的功能卻想要用同樣的方法名稱,可以完全覆寫父類別的方法。 繼承種類 一個父親可以生很多孩子,但是孩子只能有一個父親。 extends 只要透過 extends 關鍵字,子類別將可以擁有父類別的所定義屬性欄位、方法等功能。 語法 ``` class 父類別名稱{ // todo sth... } class 子類別名稱 extends 父類別名稱{ // todo sth... } ``` 範例 ``` //父類別 class Animal{ int weight;//體重 int age;//年齡 //移動 void move(){ // todo sth... } } //子類別 class Dog extends Animal{ // todo sth... } //子類別 class Bird extends Animal{ void fly(){ } } ``` 父類別,有點像公版或是一個很底層的基礎建設,大家都用的到,可以將共用的提出來放在父類別上,不用各自子類別又重覆定義,寫相同的程式碼。以此範例,我們先定義一個 Animal 動物的父類別,父類別有體重和年齡的屬性欄位,還有一個移動的方法。當子類別繼承此父類別,將擁有這些屬性欄位和方法。而各自子類別,可以再根據當下的功能需求,各自定義自己的功能。 再舉一例,如果你覺得 Java 原生的按鈕很難用,你可以繼承原本按鈕的功能外,可以在為自己或客戶打造更好用的按鈕。 super 和 this super 關鍵字:透過 super 關鍵字來存取父類別屬性欄位或呼叫父類別方法。 this 關鍵字:透過 this 關鍵字,指定為自己的屬性欄位或自己的方法。 範例: ``` class Animal { void eat() { System.out.println("Animal : eat"); } } class Dog extends Animal { void eat() { System.out.println("Dog : eat"); } void eatTest() { this.eat(); // this 呼叫自己的方法 super.eat(); // super 呼叫父類別的方法 } } public class HKTDemoJava { public static void main(String[] args) { Animal animal = new Animal(); animal.eat(); Dog dog = new Dog(); dog.eatTest(); } } ``` 多層初始化 我們在「類別」一節中,了解到,當我們將類別實體化成物件時,會進行建構子初始化的動作。而如果是多層繼承,初始化順序,當然是從父親先初始化在繼續往下子類別延續下去初始化。 範例: ``` class A{ A(){ System.out.println("執行A建構子..."); } } class B extends A{ B(){ System.out.println("執行B建構子..."); } } class C extends B{ C(){ System.out.println("執行C建構子..."); } } class HKTDemoJava { public static void main(String[] args) { C c = new C(); } } 執行結果: 執行A建構子... 執行B建構子... 執行C建構子... ``` 多型操作、抽象方法與抽象類別 如果類別與類別之間有繼承關係,則您可以用父類別宣告一個參考名稱,並讓其參考至子類別的實例,並以父類別上的公開介面來操作子類別上對應的公開方法,這是多型( Polymorphism )操作的基本方式。 更進一步的,您可以在父類別中事先規範子類別必須實作的方法,父類別中暫時無需實作,這樣的方法稱之為抽象方法( Abstract method ),它的目的是先規範操作介面,並在執行時期可以操作各種子類別的實例,包括有抽象方法的類別則稱之為抽象類別( Abstract class )。 • 多型導論 在前一個小節您知道在 Java 中,Object 類別是所有類別最頂層的父類別,您可以使用 Object 類別來宣告一個參考名稱,並讓它參考至所有的物件,例如以下的程式碼是可行的: 1 Object obj = "Java is funny!"; "Java is funny!"在執行時期會產生一個 String 實例作為代表,您可以讓它被 Object 所宣告的名稱 obj 參考而不會有任何的錯誤,在 Java 中您可以讓父類別所宣告的參考名稱,參考至子類別的實例。 Object 上有 toString()、hashCode()等方法,而 String 也繼承了這些方法,由於擁有同樣的操作方法,因而您可以透過obj 名稱來操作 toString()、hashCode()等方法,例如程式碼11-10 所示範的: ``` public class PolymorphismDemo { public static void main(String[] args) { Object obj1 = new String("Java Everywhere"); Object obj2 = new String("Java Everywhere"); System.out.println(obj1.toString()); System.out.println(obj1.hashCode()); System.out.println(obj1.equals(obj2)); } } ``` 程式碼 11-18 PolymorphismDemo.java bj1 名稱是 Object 型態,即使如此您仍可以正確的操作String 實例上擁有的共同方法,即 Object 上已規範的方法,所以程式碼第 8 行會顯示"Java Everywhere" ,第 9 行會顯示雜湊碼,第 10 行會進行字串內容值的比較,結果如下所示: 圖11-21 程式碼 11-18 的執行結果 像這樣透過 Object 型態的參考名稱來操作 String 子類的實例,就是多型(Polymorphism)操作的一個實際例子,由於您所使用的是 Object 型態的介面,所以無法操作 String 子類別上自己定義的 toUpperCase()、toLowerCase()等方法,Object 型態的名稱並不認識那些方法。 圖11-22 子類別上特定的方法無法透過父類介面來操作 如果您要操作子類別上的特定方法時,您要先轉換操作的介面,例如: ``` Object obj = "Java is funny!"; String str = (String) obj; System.out.println(str.toUpperCase()); ``` 這一次使用的是 String 態的參考名稱,因而可以正確操作toUpperCase() 方法了,結果會在螢幕上顯示 "JAVA IS FUNNY!"的文字。 圖11-23 使用 String 參考名稱來操作 String 物件 • 抽象方法、抽象類別 在定義類別的時候,您也許會先定義出一些子類別必須共同遵守的行為,但父類別的目的只是先定義,在父類別中並不打算實作這些行為,此時您可以將這些行為定義為抽象方法( Abstract method )。 在定義方法時,您可以使用關鍵字 abstract 來修飾它成為抽象方法,而一個含有抽象方法的類別則稱之為抽象類別( Abstract class ),抽象類別不能被實例化,它只能被繼承,繼承抽象類別的子類別必須實作抽象類別中所有的抽象方法。 程式碼 11-19 示範了抽象方法與抽象類別的一個實際例子,您設計了一個 GuessGame 類別,當中在 start()方法中先定義了猜數字的遊戲流程,但對於如何取得使用者輸入,以及如何顯示猜錯或猜對的訊息等方法,您希望子類別中自己定義。 ``` public abstract class GuessGame { private int number; public void setNumber(int number) { this.number = number; } public void start() { int guess = 0; do { guess = userInput(); if (guess > number) { bigger(); } else if (guess < number) { smaller(); } else { right(); } } while (guess != number); } // 抽象方法 protected abstract void bigger(); protected abstract void smaller(); protected abstract void right(); protected abstract int userInput(); } ``` 程式碼 11-19 GuessGame.java 程式碼第 1 行使用 abstract 將類別修飾為抽象類別,而程式碼第 24 行到第 27 行則使用 abstract 定義了抽象方法,可以看到抽象方法並不用實作,直接以分號作結即可。 抽象類別無法被實例化,使用它的方法是繼承它,例如程式碼 11-20 繼承 GuessGame 實作一個文字模式下的猜數字遊戲。 ``` public class TextGuessGame extends GuessGame { private java.util.Scanner scanner; public TextGuessGame() { scanner = new java.util.Scanner(System.in); } // 實作抽象方法 public void bigger() { System.out.println( "輸入數字比較目標數字大"); } public void smaller() { System.out.println( "輸入數字比較目標數字小"); } public void right() { System.out.println("恭喜!猜中了!"); } public int userInput() { System.out.print("輸入數字: "); return scanner.nextInt(); } } ``` 程式碼 11-20 TextGuessGame.java 繼承了 GuessGame 的 TextGuessGame 類別必須實作bigger()、smaller()、right()與 userInput()方法,您可以在程式碼 11-21 中看到,子類別如何實作,對於父類別中定義的流程並不影響。 public class GuessGameDemo { public static void main(String[] args) { GuessGame game = new TextGuessGame(); game.setNumber(456); System.out.println("猜數字 0.1 版..."); game.start(); } } 程式碼 11-21 GuessGameDemo.java 注意到名稱 game 的型態是 GuessGame , 由於TextGuessGame 物件的所有方法在父類別中都有規範,因而 game 可以操作 TextGuessGame 上的所有方法,這是多型操作的例子,一個執行的範例如下: 圖11-24 程式碼 11-21 的執行結果 如果今天您打算製作一個視窗模式的猜數字遊戲,您可以繼承 GuessGame 類別實作一個 WindowGuessGame,並在抽象方法中實作視窗模式的訊息顯示,最後將程式碼 11-21 的第一行改為以下: 1 GuessGame game = new WindowGuessGame(); 其餘的程式無須修改,就可以直接操作 WindowGuessGame 物件來完成視窗模式的猜數字遊戲。