介紹繼承的概念及程式的撰寫方式。
為什麼需要繼承? Why Inheritance?
試想一種情況:
有一個Aminal類別,它的定義如下:
Copy 屬性:
重量、身長、年齡。
方法:
移動。
好,這個類別定義好,現在要再定義一個Dog的類別:
Copy 屬性:
重量、身長、年齡、毛色。
方法:
移動、吃、睡、吠叫。
有沒有發現到狗的定義中,許多東西都跟動物重複。
在現實中,狗是一種動物,應該擁有動物的屬性及方法,然後再加上狗專屬的屬性和方法。
以這個例子來說,我們可以把動物當成父類別(或稱超類別super class),狗『繼承』動物,狗是『子類別 subclass』。
子類別會擁有父類別的所有屬性、方法,再加上自己定義的屬性及方法,所以可以說子類別是父類別的延伸(extend)。 (這句話超重要!多個看幾十次,想一想)
類別圖:
這是UML (統一建模語言,Unified Modeling Language) 的類別圖(簡易版),常用來描述類別之間的關係。 實心箭頭表示繼承關係,由子類別指向父類別。
圖中讀作 Dog 繼承 Animal。 另外一種常見的說法是: Dog is a Animal.
繼承的概念用『is a』來表述。 反過來說 Animal is a Dog.是不成立的,利用『is a』可以幫助思考。
Java程式要如何表示繼承關係呢? 利用關鍵字 extends:
Copy class 子類別 extends 父類別{
// code
}
沒錯,就是延伸(extends),父類別定義的東西,子類別只要繼承就等於全部擁有了,然後以父類別擁有的成員為基本,再延伸出自己特有的東西。
以上面假設的 Animal 與 Dog 類別來看,程式會長這樣:
Copy class Animal {
int height;
int weight;
int age;
void move (){
}
} // end of class Animal
class Dog extends Animal {
Color hair;
void eat (){
}
void sleep (){
}
void bark (){
}
} // end of class Dog
有沒有稍微體會到繼承的方便性呢?
一般化、特殊化
我們可以將繼承想成是一般化(Generalization)與特殊化(Specialization)的關係,繼承樹上越頂端的父類別擁有越『一般』的特性,越底端的子類別越『特殊』。
子類別擁有父類別定義的所有成員,再多了自己特有的東西。
看起來就像這樣:
父類別的功能少,子類別的功能多,不要因為父類別的英文是(super)就覺得比較厲害。
萬物之父 Object
我們知道Java是純物件導向的程式語言,而每個類別,包括自訂的類別,都繼承Object。
特別要提的是Java只支援『單向繼承』,也就是說一個子類別只可以有一個父類別,不過一個父類別可以被多個子類別繼承。
定義類別的時候,如果沒有使用關鍵字extends,Java會自行extends Object
:
Copy class Animal {
// code
}
// 等價於下面敘述,Java會自動幫你extends Object
class Animal extends Object {
// code
}
關鍵字 this 、 super
this跟super都是關鍵字,都是reference。 指到哪裡呢?
this
指到自己,也就是自己類別的成員。
Copy class Human {
String name;
int age;
Human ( String str){
this . name = str;
}
String getName (){
return this . name ;
}
}
上述程式中, this.name
意思是『自己這個類別的成員name』,當然在這個情況不寫也無所謂,但繼承關係越複雜的情況下這樣寫法可以大大增加程式的可讀性。
自己的建構字 this(.);
如果寫了很多建構子提供多元的建構物件方式,建構子之間彼此可以互相呼叫:
Copy class Human {
String name;
int age;
static int totalCount = 0 ;
Human (){
name = "untitled" ;
age = - 1 ; // 使用-1來標記沒有被設定,否則會初始化為0,但人類有可能0歲
totalCount ++ ;
}
Human ( String str){
this ();
this . name = str;
}
Human ( String str , int a){
this (str);
this . age = a;
}
void printInfo (){
System . out . println (name + " 年齡:" + age + " 目前總人數:" + totalCount);
}
}
上述程式中,this()
表示呼叫自己不帶參數的建構子,this(String)
表示呼叫自己帶有一個字串參數的建構子,以此類推。
這樣寫的好處是,各建構子之間有功能擴充的效果,已經寫好的程式可以被充分的再利用,要修改某個環節也比較不會出錯。
特別要注意的是:
this(.)
建構子只能放在第一行!!!
『Constructor call must be the first statement in a constructor.』
Copy Human( String str) { // 如果這樣寫會編譯錯誤,底下兩行位置需要互換
this . name = str;
this (); // 因為要讓建構子跑完,初始化好東西,才能做後續的設定
}
好,那用定義好的3個建構子來測試一下程式:
Copy class Test {
public static void main ( String [] args){
Human h1 = new Human() ;
h1 . printInfo ();
Human h2 = new Human( "小木" ) ;
h2 . printInfo ();
Human h3 = new Human( "小婷" , 18 ) ;
h3 . printInfo ();
} // end of main(String[])
}
執行結果:
Copy untitled 年齡: - 1 目前總人數: 1
小木 年齡: - 1 目前總人數: 2
小婷 年齡: 18 目前總人數: 3
super
指到父類別,使用方法跟this類似。
Copy class Animal {
int height;
int weight;
static int totalCount = 0 ;
Animal () {
this ( - 1 , - 1 );
}
Animal ( int h) {
this (h , - 1 );
}
Animal ( int h , int w) {
this . height = h;
this . weight = w;
totalCount ++ ;
}
String getInfo () {
return "身長:" + height + " 重量:" + weight;
}
} // end of class Animal
class Dog extends Animal {
String color;
static int totalCount = 0 ;
Dog () {
this ( - 1 , - 1 , "noset" );
}
Dog ( int h , int w) {
this (h , w , "noset" );
}
Dog ( String c) {
this ( - 1 , - 1 , c);
}
Dog ( int h , int w , String c) {
super(h , w);
this . color = c;
totalCount ++ ;
}
String getInfo () {
return super . getInfo () + " 毛色:" + this . color ;
}
} // end of class Dog
程式有點長,慢慢看沒關係。
Animal中帶有兩個參數的建構子 Animal(int h,int w)
視為主要負責初始化功能的運算子,其他參數比較少的建構子就負責呼叫這個建構子。
Dog的建構子中,主要運做的是 Dog(int h,int w,String c)
,其他參數比較少的運算子只需要設計應該帶什麼參數給他。 這種被稱為方法的包裝(wrapped) 是常見且比較容易設計的做法。
父類別的建構子 super(.)
利用super(.)可以呼叫父類別中定義好相應參數的建構子,那為什麼還要特地呼叫父類別的建構子呢? 很多時候父類別已經定義好的東西,子類別直接用就好,設計上比較好維護,設計邏輯比較有階層性。
覆寫 Override
再來看getInfo()方法,Animal裡面已經定義了一個,依據繼承的理論,Dog繼承Animal應該不用自己寫也會有一個getInfo()才對。 沒錯,Dog如果不自己定義會有一個跟Animal『一模一樣』的getInfo()方法,但很明顯,父類別太過於一般化,沒辦法滿足子類別需要的功能(以此為例就是資訊量不夠),所以子類別『覆寫(override)』 了父類別的方法,創造了特殊且適合自己的getInfo()。
super.方法()、super.欄位,就是呼叫父類別那邊的方法、欄位,當然前提是存取修飾子允許你看到。
測試一下執行結果:
Copy class Test {
public static void main ( String [] args) {
Dog d1 = new Dog() ;
System . out . println ( d1 . getInfo ());
Dog d2 = new Dog( 30 , 10 ) ;
System . out . println ( d2 . getInfo ());
Dog d3 = new Dog( "white" ) ;
System . out . println ( d3 . getInfo ());
Dog d4 = new Dog( 30 , 10 , "white" ) ;
System . out . println ( d4 . getInfo ());
System . out . println ( "動物數量:" + Animal . totalCount );
System . out . println ( "狗狗數量:" + Dog . totalCount );
} // end of main(String[])
} // end of class Test
執行結果:
Copy 身長: - 1 重量: - 1 毛色:noset
身長: 30 重量: 10 毛色:noset
身長: - 1 重量: - 1 毛色:white
身長: 30 重量: 10 毛色:white
動物數量: 4
狗狗數量: 4
層層初始化
我們知道子類別擁有父類別的所有程式碼,那初始化順序呢?
初始化子類別前,還要先初始化父類別,畢竟父類別建構出來才有足夠的基礎資料去建構『延伸』的部份,所以每個類別要建構的時候,都會往上追朔,追朔到 Object 開始一層一層建構下來,也是因為這樣,子類別才能擁有父類別的所有成員。
舉個例子:
Copy 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的建構子" );
}
}
建構一個C物件試試:
Copy class Test {
public static void main ( String [] args) {
C c = new C() ;
} // end of main(String[])
} // end of class Test
執行結果:
Copy 這裡是A的建構子
這裡是B的建構子
這裡是C的建構子
哇塞,真是太神奇了!
看一下類別示意圖:
我們要建構的是C,而C是B的延伸,所以要先有B,而B是A的延伸,所以要先有A,而A是Object的延伸,所以要先有Object。 於是就從最頂端的父類別一直建構下來。
好,現在我知道需要從父類別初始化下來,但建構子呢? 一個類別可以定義無數個建構子,他怎麼知道我要用哪個建構子來建構我的物件? 到底是以什麼機制來建構父類別的?
嗯,回想一下,當初在定義類別的時候,如果沒有定義任何建構子,Java會幫你定義一個不帶參數不做任何事的建構子,現在同樣的老招又來一次!
只要你的建構子中『沒有呼叫任何建構子』,就會在『第一行』偷偷幫你家上去一個 super();
有多偷偷呢? 你連看都看不到!! 但他就是存在於最後的程式碼中。
以上的程式來說,就像這樣:
Copy class A {
A (){
super(); // 這行不寫的話,Java會幫你加上,但你看不到
System . out . println ( "這裡是A的建構子" );
}
}
class B extends A {
B (){
super(); // 這行不寫的話,Java會幫你加上,但你看不到
System . out . println ( "這裡是B的建構子" );
}
}
class C extends B {
C (){
super(); // 這行不寫的話,Java會幫你加上,但你看不到
System . out . println ( "這裡是C的建構子" );
}
}
好的,現在知道他會自動幫我呼叫super();
來建構父類別,但是如果我不想用這個不帶參數的建構子呢? 我辛苦設計那麼多建構子,他只會幫我呼叫不帶參數的,太慘了吧!
嗯嗯,沒錯就是這麼慘,所以如果要呼叫有帶參數的super(.);
你就要自己寫!
觀察底下程式,想想執行結果:
Copy class A {
A (){
System . out . println ( "這裡是A的建構子" );
}
}
class B extends A {
B (){
System . out . println ( "這裡是B的建構子" );
}
B ( String str){
this ();
System . out . println ( "嗨這裡是B:" + str);
}
}
class C extends B {
C (){
this ( "hello tina" );
System . out . println ( "這裡是C的建構子" );
}
C ( String str){
super(str);
System . out . println ( "嗨這裡是C:" + str);
}
}
主程式:
Copy class Test {
public static void main ( String [] args) {
C c = new C() ;
} // end of main(String[])
} // end of class Test
執行結果:
Copy 這裡是A的建構子
這裡是B的建構子
嗨這裡是B:hello tina
嗨這裡是C:hello tina
這裡是C的建構子
如果跟你想的不一樣,在重新看一下上面的描述再想想,哪理卡卡的可以問我。 這裡是重要的繼承觀念。
存取修飾子 protected
在存取修飾子 的章節提過,現在剛好提到繼承再拿出來討論。
protected是個關鍵字,開放的最大權限為『不同套件的子類別』可以存取。
假設 Animal 與 Dog 位在不同package,先看Animal的程式碼:
Copy package A ;
public class Animal {
public String name; // 4個屬性剛好4種權限範圍都做測試
protected int height;
int weight;
private int age;
// ↓這個修飾子一定要public或protected,不然不同類別的Dog不能用他來建構物件
public Animal ( String str , int h , int w , int a){
this . name = str;
this . height = h;
this . weight = w;
this . age = a;
}
}
再看Dog的程式碼:
Copy package B ;
import A . Aminal ;
public class Dog extends Animal {
String color;
public Dog ( String str , int h , int w , int a , String c){
super(str , h , w , a);
this . color = c;
}
public void printInfo (){
System . out . println (name); // OK, public 不同套件也可以存取
System . out . println (height); // OK, protected 允許不同套件子類別存取
System . out . println (weight); // 編譯錯誤,預設只有同 package 可以存取
System . out . println (age); // 編譯錯誤, private 只有自身類別能存取
System . out . println (color); // OK, 自己類別定義的成員當然OK
}
}
覆寫的存取修飾限制
上面的範例程式有稍為提到過覆寫(override),這邊再詳細討論一下,以及一些限制。
覆寫 Override,字面上的意思就是『覆蓋重寫』。
在繼承中關係,父類別定義了一些方法,子類別覺得不適用的話可以『覆蓋』掉父類別的方法,然後『重寫』屬於自己的方法。
舉個例子:
Copy class A {
void printInfo (){
System . out . println ( "hello, I am A." );
}
}
class B extends A {
void printInfo (){
System . out . println ( "hello, I am B." );
}
}
class C extends A {
}
測試程式:
Copy class Test {
public static void main ( String [] args) {
B b = new B() ;
b . printInfo ();
C c = new C() ;
c . printInfo ();
} // end of main(String[])
} // end of class Test
執行結果:
Copy hello , I am B .
hello , I am A .
上述程式中,B與C都是繼承A,表示擁有了A所有的成員,但B覆寫了printInfo()方法,而C沒有。 所以在呼叫的時候,物件b會使用B類別覆寫的方法,而物件c因為C類別沒有自己定義,所以會使用到父類別A所定義的printInfo()。
好,那來談談覆寫的限制。
要覆寫父類別方法必須滿足幾個條件:
子類別覆寫的方法名稱、回傳型態、參數個數順序需相同。
子類別覆寫的方法,其開放權限不可以小於要覆寫的父類別方法。
第一點,用final修飾的方法無法被覆寫。
這是關鍵字final修飾方法的特性,詳細內容於後面討論。
第二點,方法名稱、回傳型態、參數個數必須相同。
嗯,如果不一樣的話,就是自己再定義一個新方法了阿!!跟覆寫有什麼關係 XD
第三點,子類別方法開放權限不得小於父類別方法。
簡單來說,如果父類別說這個方法是對全世界公開(public)的方法,你要覆寫就不能占為己有(private)。
※存取修飾子的開放權限從大到小 :public -> protected -> (no modifier) -> private。
如果父類別說此方法是protected,那子類別覆寫時的修飾子必須是public或protected。
如果父類別說此方法是private,那子類別覆寫時的修飾子必須是public或protected或(no modifier)或private。
關鍵是權限的開放範圍不得小於覆寫對象。
以下針對三種限制用程式來說明~
第一點,程式範例:
Copy class A {
// (↓關鍵字 final)
public final void printInfo (){
System . out . println ( "hello, this is A." );
}
}
class B extends A {
// 編譯錯誤! ↓ 利用final修飾的方法不能被覆寫。
public void printInfo (){
System . out . println ( "hello, this is B;" );
}
}
在類別A的printInfo()方法利用關鍵字 final 修飾,所以任何繼承他的子類別都不能覆寫這個方法。 否則會產生編譯錯誤:『Cannot override the final method from A』。
第二點,程式範例:
Copy class A {
public void printInfo (){
System . out . println ( "hello, this is A." );
}
}
class B extends A {
public void printInfo2Tina (){
System . out . println ( "hello Tina, nice to meet you <3" );
}
}
恩,就是多定義一個方法,沒什麼好說的,這根本不是覆寫。
第三點,程式範例:
Copy class A {
// 注意存取修飾子是(no modifier)
void printInfo (){
System . out . println ( "hello, this is A." );
}
}
class B extends A {
// ↓ 編譯錯誤,覆寫的方法存取權限小於覆寫對象
private void printInfo (){
System . out . println ( "hello, this is B." );
}
}
在A類別中的printInfo()方法修飾子是(no modifier),依據覆寫的開放權限規則,B類別繼承了A類別想覆寫printInfo(),覆寫的開放權限必須為public或protected或(no modifier),重點就是不能小於覆寫對象,否則會發生編譯錯誤:『Cannot reduce the visibility of the inherited method from A』。