# 物件導向的三大特性 : 封裝,繼承,多型
物件導向的三大特性:封裝、繼承、多型
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 物件來完成視窗模式的猜數字遊戲。