作者 : 許裕永
繼承(Inheritance)是物件導向的重要特性,就是撰寫類別時,替類別指定一個上層類別。則下層類別(sub class)便繼承了上層類別 (super class)中的所有成員(不包含建構方法)。也就是說:「下層類別不必再重複撰寫上層類別中,已經撰寫過的程式碼,只要撰寫屬於自己的程 式碼」。也可以說:「下層類別擴充了上層類別的功能」。
實務上,我們把大部份類別,都需要的資料成員及方法成員,撰寫成一個上層類別。那麼在撰寫其他類別時,只要繼承此上層類別,則各下層類別之中,便可以省略 在上層類別中,已經宣告過的資料成員及方法成員。而可以把宣告在上層類別中的成員,當作是下層類別中自己宣告的成員來存取。如此在開發大型專案時,只要規 劃得當,這樣一層一層的繼承機制,省下的時間,就非常可觀了。
換個角度來看,上層類別中宣告的成員,下層類別可以繼承,也可以看成上層類別中宣告了下層類別一定要有的成員,想要沒有都不行。也就是說:上層類別中宣告了下層類別一定要擁有的資料成員及方法成員。
實務上,我們把對大部份類別的限制,撰寫在上層類別之中,可以限定下層類別開發時,一定會符合上層類別的規範,如此可以減少日後大型專案整合時的邏輯錯誤。
下層類別繼承了上層類別中的那些成員?答案是:「除了建構方法之外的所有成員,只是並非所有成員都可以任意存取」。上層類別中宣告為private的成 員,下層類別有繼承,但是不能存取。上層類別中宣告為protected的成員,不同package的下層類別,可以在類別中以直接存取的方式存取,但是 不能建構成物件之後以物件存取,同一個package的下層類別可以用任意方式存取。上層類別中宣告為public的成員,下層類別中可以用任意方式存 取。上層類別中未宣告存取限制的成員,同一個package的下層類別可以用任意方式存取,不同package的下層類別中,不可以存取。
為了規範所有Java的類別,必需具備的功能及規格,Java提供了一個類別:Object。它是Java所有類別的共同上層類別。即使我們開發的新類別 沒有宣告繼承,Java也會讓此新類別繼承Object。也就是說:只要是Java的類別,就一定擁有Object中宣告的成員。
1-1 重要方法
方法一:protected Object clone()
複製執行本方法的物件。
方法二:boolean equals(Object obj)
參數物件與本物件是否為同一個物件。
方法三:protected void finalize()
本物件被消毀時,Java會呼叫的方法。
方法四:int hashCode()
本物件的雜湊碼。
方法五:void notify()
通知物件離開等待狀態(執行緒同步化時使用)。
方法六:void notifyAll()
通知所有本類別物件離開等待狀態(執行緒同步化時使用)。
方法七:String toString()
描述本物件的文字。
方法八:void wait()
方法九:void wait(long timeout)
方法十:void wait(long timeout, int nanos)
讓物件進入等待狀況(執行緒同步化時使用)。
以上方法,有些會在本章後續節次中介紹,有些會在本書後續章節中介紹,讀者只要先有概念即可。
SuperClassName:上層類別名稱。每個類別只能有一個上層類別,若沒有宣告上層類別,則此類別的上層類別預設為Object。
例一:
public class SubClass{
public static void main(String[] args){
SubClass sc=new SubClass();
System.out.println(sc.hashCode());
System.out.println(sc.toString());
}
}
列印結果:3526198 SubClass@35ce36。
示範主題:上層類別宣告為public的成員,也就是下層類別public的成員,可以用物件存取。
例二:
public class SubClass{
public int myHashCode(){
return hashCode()*5;
}
public String myToString(){
return "Yung " + toString();
}
public static void main(String[] args){
SubClass sc=new SubClass();
System.out.println(sc.myHashCode());
System.out.println(sc.myToString());
}
}
列印結果:17630990 Yung SubClass@35ce36。
示範主題:上層類別宣告的成員(非private),可以在下層類別中直接存取(呼叫)。
例三:
class SuperClass{}
public class SubClass extends SuperClass{
public int myHashCode(){
return hashCode()*5;
}
public String myToString(){
return "Yung " + toString();
}
public static void main(String[] args){
SubClass sc=new SubClass();
System.out.println(sc.myHashCode());
System.out.println(sc.myToString());
}
}
列印結果:17630990 Yung SubClass@35ce36。
示範主題:1、用extends指定上層類別。2、Object是SuperClass的上層類別,它的方法被SuperClass繼承後,會再被SubClass繼承,這是所謂「多層繼承」。
請注意本例可以撰寫於同一個程式碼檔,但SuperClass不可以宣告為public,而且檔案名稱一定是SubClass‧java。
Object是SuperClass的上層類別,也是SubClass的上層類別(Java中沒有祖類別這個名詞)。所以才會說Object是Java中所有類別的上層類別。
3-1 限制與注意事項
我們可以先想想看,Java為什麼要把Object設為所有類別的上層類別?以類別開發者的角度來看,它限制了所有類別一定要擁有Object中的方法成 員;但以類別使用者的角度來看,任何類別所建構的物件都可以呼叫那些Object中宣告的方法,來執行特定功能,或取得指定的資料。
當我們面對一個陌生的類別時,了解這個類別最直接的方式,就是建構這個類別的物件後,呼叫這個物件的toString()。這個物件一定有toString()嗎?一定有,因為toString()是Object繼承下來的。
Date d=new Date();
System‧out‧println(d);
還記得這兩行程式碼嗎?把d物件列印出來,物件怎麼列印?在println中的參數,表示是要列印在螢幕中的文字,所以Java會自動呼叫d物件的 toString()。由此可見,toString()是Java定義在Object中,讓所有類別建構的物件,都可以用來顯示代表該物件的文字的方法成 員。任意物件,只要是在需要轉換為字串的運算式之中,Java都會自動呼叫該件的toString()。
可是,要代表不同物件的文字不是不一樣嗎?怎麼可能定義在Object中的一個方法就可以讓任意Object的下層類別建構的物件,能各自顯示不同的資訊呢?之前我們做的範例,呼叫toString()只是顯示它的物件代號,並沒有顯示什麼資訊啊?
這就是overriding的必要性。每個類別,可以把從上層類別繼承過來的成員,重新定義。在Object中宣告的方法,它們定義的內容都是基礎功能, 基本上和我們開發的類別幾乎是沒什麼關係。但它至少讓本類別擁有了這些方法,讓我們可以在類別中自行overriding有必要重新定義的方法;讓本類別 建構的物件,呼叫這個方法的時候,執行的是本類別中自行定義的內容。
例一:
public class SubClass{
int a;
int b;
public String toString(){
return "a=" + a + ";b=" + b;
}
public static void main(String[] args){
SubClass sc=new SubClass();
System.out.println(sc.toString());
}
}
列印結果:a=0;b=0。
示範主題:物件執行的是本類別中重新定義的內容。
overriding和overloading是完全不一樣的。overloading機制下,各個方法的名稱一樣,但參數列卻一定不一樣。 overloading機制下的方法,每一個都是獨立的方法,沒有取代的問題,它們是同時存在,然後依照呼叫敍述句的參數,來執行相對應的方法。而且 overloading可以運用在開發的類別本身,也可以在下層類別中overloading上層類別的方法。
但是overriding是有取代性的,定義的新內容會取代舊的內容。而且只能運用在下層類別overriding上層類別的方法。而且限制相當嚴格:
例:
上層類別:
Object test(){}
下層類別:
Integer test(){}//合法
String test(){}//合法
int test(){}//不合法,AutoBoxing在此處不適用
(private->default->protected->public)
例:
上層類別:
protected void test(){}
下層類別:
protected void test(){}//合法
public void test() {}//合法
void test(){}//不合法,不宣告即為default。
例:
上層類別:
void test()throws IOException,SQLException{}
下層類別:
void test()throws IOException{}//合法
void test()throws SQLException{}//合法
void test()throws IOException,TimeoutException{}//不合法
例:
上層類別:
void test()throws IOException {}
下層類別:
void test()throws FileNotFoundException{}//合法
void test()throws Exception{}//不合法
下層類別中的類別方法成員的名稱,可以和上層類別中的類別方法成員的名稱一樣。但這不是overriding。因為這些方法是屬於類別,不屬於物件,和物件無關。
針對第一種抽象類別,下層類別繼承後,只要下層類別不要宣告abstract,則下層類別就不是抽象類別。
但是第二種抽象類別就比較複雜,下層類別繼承時,便繼承了上層類別中宣告的抽象方法,所以下層類別中就擁有了抽象方法。此時有兩種處理方式:1、將下層類 別也宣告為抽象類別,禁止建構成物件。2、overriding所有抽象方法,讓下層類別中,沒有抽象方法,則下層類別便不必宣告為抽象類別。
下層類別繼承了一般類別之後,是否overriding上層類別的方法,可以自行決定。但是,若繼承了抽象類別,則是一定要overriding抽象方法。也就是說:抽象類別規範了下層類別一定要定義的方法。
例:
上層類別:
abstract class SuperClass{}
下層類別:
class SubClass extends SuperClass{}//合法
abstract class SubClass extends SuperClass{}//合法
上層類別:
abstract class SuperClass{
abstract public void value();
}
下層類別:
abstract class SubClass extends SuperClass{}//合法
class SubClass extends SuperClass{
public void value(){}
}//合法,已重新定義(定義為不執行任何敍述句)
class SubClass extends SuperClass{
public void value(int a){}
}//不合法,未重新定義(此程式碼為overloading)
class SubClass extends SuperClass{
void value(){}
}//不合法,上層類別中的存取修飾詞為public,下層類別不宣告代表
//default,其限制性嚴格於public。
3-2 overriding equals
物件的比對有兩種,第一種是比對:「是否為同一個物件」。第二種是比對:「兩個物件的值是否相同」。
例:
String a=new String(”Java”);
String b=new String(”Java”);
System.out.println(a==b);
System.out.println(a.equals(b));
列印結果:false true。
上面的範例程式碼,在很久很久以前我們做過。我們也得到一個結論,用“==”是比較參考變數a及參考變數b是否代表同一個物件,equals用來比較物件 值是否相同。這個結論,對String類別及Wrapper Classes類別所建構的物件而言,是對的。但是對其他類別所建構的物件,則未必。例 StringBuffer,雖然一樣是運算字串值的類別,但是此類別的物件呼叫equals()時,比較的並不是物件的值,而是和“==”一樣,比對是否 為同一個物件。
任何類別從Object繼承來的equasl(),其預設的功能都是用來比對是否為同一個物件,也就是和“==”的比對方式是一樣的。但是開發類別的時 候,若是希望本類別的物件呼叫equals()時,可以用特定的方式比對,那就要在類別中overriding equasl()。至於Java提供的類 別中,讀者只要知道String及Wrapper Classes有overriding equals()就可以,其他類別,自行測試吧!
例一:
public class SubClass extends SuperClass{
int a;
public boolean equals(Object o){
if(o instanceof SubClass && ((SubClass)o).a==this.a){
return true;
}else{
return false;
}
}
public static void main(String[] args){
SubClass sc=new SubClass();
sc.a=10;
SubClass sc2=new SubClass();
sc2.a=10;
System.out.println(sc==sc2);
System.out.println(sc.equals(sc2));
}
}
列印結果:false true。
示範主題:類別overriding equals()。
equals中的判斷句內容,是正確的寫法,而且一定要依照此精神撰寫,讀者可以先略過,等了解後續章節中介紹的多型之後,再回頭研究。但是,如果不按照此精神,只要運算結果是布林值,編譯還是會過,但是日後的邏輯錯誤,可就夠你頭大了。
例二:
public boolean equals(SubClass o){
if(o instanceof SubClass && ((SubClass)o).a==this.a){
return true;
}else{
return false;
}
}
將例一中equals(Object o)修改為equals(SubClass s),其他不變更。
列印結果:false true。
示範主題:失敗的overriding。
總麼失敗了?列印結果不是一樣嗎?錯!大錯!非常錯!這是overloading不是overriding。Obejct中的equals的參數設為 Object型別,下層類別overriding時,自然也要把參數設為Object型別,因為本方法要能夠接受任何型別的物件為參數。以本例的寫法,若 是參數為本類別物件,會呼叫本例撰寫的方法,但若是參數為非本類別物件時,呼叫的卻是Object預設的equals(),也就是這個類別的 equals()有兩個,而日後你可能會花一個月的時間,才找得到這種邏輯錯誤,寃啊!
3-3 overriding hashCode
hashCode()的運算結果是一個代表本物件的整數值,它是Java執行內部運算時,識別物件的依據之一(例:部份集合物件中,用來排列的依據)。如果此方法的運算結果和Java期望的不一樣,可能會造成某些運算的錯誤。
例一:
public class SubClass{
int a;
int b;
public int hashCode(){
return (a*b)^8;
}
public static void main(String[] args){
SubClass sc=new SubClass();
sc.a=10;
sc.b=20;
SubClass sc2=new SubClass();
sc2.a=20;
sc2.b=30;
System.out.println(sc.hashCode());
System.out.println(sc2.hashCode());
}
}
列印結果:192 592。
示範主題:overriding hashCode()。
hashCode()的運算結果只要是int值,編譯便合法,但卻不見得符合Java的期望。除了應該以類別中的資料成員依照固定的運算式來運算之外,Java期望你能依照下列原則來設計hashCode():
因為不一樣類別的物件呼叫hashCode()的運算結果有可能相同,但其equals()卻一定為flase。
例:
class HashCodeTest{
int a,b;
public boolean equals(Object o){
if(o instanceof HashCodeTest){
HashCodeTest hc=(HashCodeTest)o;
if(this.a=hc.a && this.b=hc.b)
return true;
}
return false;
}
public int hashCode(){
return (a+b)^8;
}
}
示範主題:以equals()中比對用的成員來運算hashCode()的值。
1‧配置上層類別宣告的實體資料成員,並給予預設值及初始化。
2‧執行上層類別的建構方法。
3‧配置下層類別宣告的實體資料成員,並給予預設值及初始化。
4‧執行下層類別的建構方法。
當然若有一層以上的上層類別,自然是從最上層的上層類別開始執行。
4-1 上層類別建構方法的宣告
下層類別建構方法執行之前,一定會呼叫上層類別的建構方法。也就是說:即使下層類別的建構方法中,沒有指定呼叫上層類別的建構方法,Java也會預設呼叫上層類別的建構方法。
但這個時候就要注意了,Java預設呼叫的是沒有參數的上層類別建構方法,如果上層類別中沒有沒參數的建構方法,就會造成編譯錯誤。
例一:
class SuperClass{}
public class SubClass extends SuperClass{
public static void main(String[] args){
SubClass sc=new SubClass();
}
}
本例編譯成功,因為下層類別預設的建構方法,執行時預設會呼叫上層類別中沒有參數的建構方法,而上層類別中有一個預設的沒有參數的建構方法。
例二:
class SuperClass{
SuperClass(int a){}
}
public class SubClass extends SuperClass{
public static void main(String[] args){
SubClass sc=new SubClass();
}
}
本例編譯錯誤,因為上層類別中已經沒有預設的沒有參數的建構方法。
例三:
class SuperClass{
SuperClass(){}
SuperClass(int a){}
}
public class SubClass extends SuperClass{
public static void main(String[] args){
SubClass sc=new SubClass();
}
}
本例編譯成功,因為上層類別中有自訂的沒有參數的建構方法。
例四:
class SuperClass{
SuperClass(int a){}
}
public class SubClass extends SuperClass{
public SubClass(){}
public static void main(String[] args){
SubClass sc=new SubClass();
}
}
本例編譯錯誤,下層類別自訂的建構方法中,沒有指定呼叫上層類別的建構方法時,預設會呼叫上層類別中,沒有參數的建構方法。
例五:
class SuperClass{
SuperClass(int a){}
}
public class SubClass extends SuperClass{
public SubClass(){
super(8);
}
public static void main(String[] args){
SubClass sc=new SubClass();
}
}
本例編譯成功,因為在下層類別的建構方法中,指定呼叫上層類別的有參數的建構方法。指定呼叫上層類別的敍述句一定要置於第一行。
結論:任何類別的建構方法中,第一個執行的敍述句,一定是呼叫上層類別的建構方法(super()),或呼叫本類別的建構方法(this())。
4-2 上層類別成員的存取
下層類別本來就可以存取繼承自上層類別中,存取限制宣告為允許下層類別存取的成員。本節指的是被下層類別overriding的成員。
例一:
class SuperClass{
private int a=5,b=8;
int getValue(){
return a*b;
}
}
public class SubClass extends SuperClass{
int c=3;
int getValue(){
return super.getValue()*c;
}
public static void main(String[] args){
SubClass sc=new SubClass();
System.out.println(sc.getValue());
}
}
列印結果:120。
示範主題:因為上層類別的資料成員均設為private,下層類別中無法存取,要取得其運算值就一定要呼叫getValue(),但下層類別中又已經 overriding getValue(),所以必須用super‧getValue()才能呼叫到上層類別定義的getValue()。
例二:
class SuperClass{
int a=5;
}
public class SubClass extends SuperClass{
int a=3;
int getValue(){
return super.a*a;
}
public static void main(String[] args){
SubClass sc=new SubClass();
System.out.println(sc.getValue());
}
}
列印結果:15。
示範主題:用super‧資料成員,也可以存取上層類別中被下層類別覆蓋的資料成員。
4-3 上層類別的初始化區塊
下層類別可以指定執行上層類別中的某一個建構方法,就代表寫在上層類別的建構方法中的程式敍述不一定會執行。那麼希望無論下層類別是否指定呼叫都一定要執行的程式敍述就可以寫在初始化區塊。當然初始化區塊也一樣分為實體及類別兩種。
例:
public class SubClass extends SuperClass{
{
System.out.println("Sub Block");
}
SubClass(){
System.out.println("Sub Constractor");
}
static{
System.out.println("Sub Static Block");
}
public static void main(String[] args){
SubClass jt=new SubClass();
SubClass jt2=new SubClass();
}
}
class SuperClass{
static{
System.out.println("Super Static Block");
}
{
System.out.println("Super Block");
}
SuperClass(){
System.out.println("Super Constractor");
}
}
列印結果:Super Static Block Sub Static Block
Super Block Super Constractor Sub Block Sub Constractor
Super Block Super Constractor Sub Block Sub Constractor
示範主題:1、無論類別或實體初始化區塊,都是先執行上層類別的區塊。2、上層類別的建構方法執行完畢之後,才執行下層類別的初始化區塊。
下層類別中的類別方法成員的名稱,可以和上層類別中的類別方法成員的名稱一樣。但這不是overriding。因為這些方法是屬於類別,不屬於物件,和物件無關。
沒有留言:
張貼留言