2018年11月13日 星期二

Java SE 入門教學 - 抽象類別與介面

更新時間:11/19/2018

前言

前一篇介紹了物件導向,類別的構造與性質,雖然知道把具體或概念一一抽象出來成為類別,碰到相似的具體或概念時,可以使用繼承來減少程式碼的撰寫,但有沒有可能您寫的父類別,繼承過來的子類別卻需要把每個方法都覆寫(Override)一次,導致父類別的方法完全無用武之地呢?

或是您可能想要繼承爸爸與媽媽的意志,爸爸是由爺爺、奶奶這個血脈傳承下來,而媽媽卻是由外公、外婆這個血脈傳承下來,那麼我要如何同時繼承兩個呢?在 JAVA 中規定不能繼承兩個類別,那怎麼辦?

一、抽象類別(abstract class)

既然父類別的方法無用武之地,那麼我們只宣告方法名稱而不實作當中的邏輯,這樣的方法稱之為「抽象方法」(Abstract method)

如果一個類別中包括了抽象方法,則該類別稱之為「抽象類別」(Abstract class),抽象類別是個未定義完全的類別,所以它不能被用來生成物件,它只能被擴充,並於擴充後完成未完成的抽象方法定義。

在 Java 中要宣告抽象方法與抽象類別,使用"abstract"關鍵字。

從某種意義上來說,抽象方法就是被用來重寫的,所以在父類別宣告的抽象方法一定要在子類別裡面重寫。如果真的不想在子類別裡面重寫這個方法,那麼可以在子類別裡面把這個方法再定義為抽象方法,因為子類別覺得我去實現也不合適,應該讓繼承我的子類別去實現比較合適,因此也可以在繼承這個子類別的下一個子類別裡面重寫父類別宣告的抽象方法,這樣是可以的。

1.1 抽象類別與方法的注意事項

◉ 若類別中有一個方法未實作任何程式碼,則該方法必須宣告為抽象方法,且此類別必須宣告為抽象類別
◉ 若父類別是一個抽象類別,其所產生的抽象方法必須由子類別來重新實作
◉ 如果子類別沒有全部實作出父類別提供的所有抽象方法,那麼子類別也必須宣告成抽象類別
◉ 抽象方法的存取修飾子不可以是 static、final、private 等關鍵字
◉ 抽象方法的宣告不可以有任何程式碼,所以連 {} 都不可以出現
◉ 抽象類別不可以使用 new 的方法建立任何物件實例
◉ 抽象類別中並不一定要有抽象方法,其目的是強迫繼承該類別

1.2 抽象類別與方法的宣告語法

◉ 抽象類別

[存取修飾子] abstract class 類別名稱 [extends <父類別>]
[implements <介面1> [, <介面2> ...] ]{
  <declarations>
}

◉ 抽象方法

[存取修飾子] abstract 傳回值型別 方法名稱(參數);

◉ 繼承抽象父類別語法

class 子類別 extends 抽象父類別 {
  實作父類別的抽象方法;
}

◉ 新認識的修飾子 abstract 和 final

1.abstract

(i) 不可寫在變數
(ii) 使用在方法上時,方法不能實作程式碼

2.final

(i) 如果寫在變數上時 → 常數,其數值不可改變
(ii) 如果寫在方法上時 → 不可被覆寫(Override)
(iii) 如果寫在類別上時 → 不可被繼承

範例:抽象類別與繼承

抽象物件要求"繼承";抽象方法要求"覆寫"。程式看到 abstract 關鍵字,就不會載入記憶體,也就是說,無法使用這段程式碼,只能繼承使用覆寫(Override)來實踐。



二、介面(interface)

JAVA 是只支持單一繼承,但現實中存在多重繼承這種現象,如"金絲猴是一種動物",金絲猴從"動物"這個類別繼承,同時"金絲猴是一種值錢的東西",金絲猴從"值錢的東西"這個類別繼承,同時"金絲猴是一種應該受到保護的東西",金絲猴從"應該受到保護的東西"這個類別繼承。這樣金絲猴可以同時從"動物類別"、"值錢的東西類別"、"應該受到保護的東西類別"這三個類別繼承,但由於 JAVA 只支持單一繼承,因此金絲猴只能從這三個類別中的一個來繼承,不能同時繼承這三個類別。

因此為了現實生活中存在的多重繼承現象,可以把其中的兩個類別變成介面。使用介面可以幫助我們實現多重繼承這種現象。

介面語法:

介面被實作

<modifier> class <name> [extends <superclass>]
[implements <interface1> [, <interface2> ...] ]{
  <declarations>
}

介面繼承介面

<modifier> interface <name> [extends <interface1> [, <interface2> ...]]{
  <declarations>
}

介面的特性:

◉ 介面所宣告的方法沒有實作內容
◉ 介面的方法由類別來實作
◉ 多個無關的類別可以實作同一個介面
◉ 一個類別可以實作多個介面
◉ 介面定義一個 JAVA 型別

介面的注意事項:

◉ 介面存取權限與一般 class 一樣,只能是 default 或 public。但當介面化身為一般內部類別的介面時,則無此限制。
◉ 介面內宣告的「屬性變數」必須自行給定初始值。因為編譯後會加上 public static final 關鍵字,也就是全域常數。
◉ 介面內宣告的「方法」撰寫上不可以加上 {} 實作程式區塊,必須以分號來結束方法的定義,且編譯後會加上 public abstract 關鍵字。

介面內宣告的所有方法都是抽象方法,宣告成 public abstract,而屬性變數(成員變數)都是靜態常數,宣告成 public static final,如果不想要寫上這些關鍵字,則什麼關鍵字都不能寫,例如:

public static final double weight; → double weight;
public abstract double bmi(); → double bmi();

之所以要這樣宣告是為了修正 C++ 裡面多重繼承的時候容易出現問題的地方,C++ 的多重繼承容易出現問題,問題在於多重繼承的多個父類別之間如果他們有相同的屬性變數的時候,這個引用起來會相當地麻煩,並且運行的時候會產生各種各樣的問題。

JAVA 為了修正這個問題,把介面裡面所有的屬性變數全都改成 public static final,屬性變數是靜態類型,那麼這個屬性變數就是屬於整個類別,而不是專屬於某個物件實例。對於多重繼承來說,在一個子類別物件實例裡面實際上包含有多個父類別物件實例,而對於單繼承來說,子類別物件實例裡面就只有一個父類別物件實例。多重繼承子類別物件實例就有多個父類別物件實例,而這些父類別物件實例之間又可能會存在有重複的屬性變數,這就非常容易出現問題。

因此在 JAVA 裡面避免了這種問題的出現,採用了介面這種方式來實現多重繼承。作為介面來說,一個類別可以實作多個介面,這也是多重繼承。介面裡面的屬性變數不專屬於某個物件實例,都是靜態的屬性變數,是屬於整個類別的,因此一個類別去實作多個介面也是無所謂的,不會存在物件實例之間互相衝突的問題。實現作多個介面,也就實現了多重繼承,而且又避免了多重繼承容易出現問題的地方,這就是用介面實現多重繼承的好處。



範例,確認方法的存取修飾子為 public

編譯錯誤,因為介面的抽象方法存取權限為 public,權限比 public 小的不能覆寫。


修正過後就可以正常執行



範例,確認屬性變數存取修飾子是否為 final
在 Shape 介面內,第2行增加一個屬性變數 double pi = 3.1415926;

在 main 方法內,第59行增加一行程式碼 Shape.pi += 10;


編譯錯誤,因為介面的屬性方法其存取權限為 public static final,其值不能改變





三、存取修飾子 final 的用法

3.1 final 類別不能被繼承


3.2 final 方法不能被覆蓋


3.3 final 變數不能被修改值



四、認識 Object 類別

Java SE 8 Object 中有說明, Object 類別是所有類別的父類別,是類別層次結構的根。在 Java 中,所有的物件都隱含繼承 java.lang.Object 類別,當您定義一個類別時:

這個程式碼相當於:

當您寫下任何類別時,類別內都會有從 Object 類別繼承過來的方法,共11個方法:

  • clone():複製此物件
  • equals(Object obj):比較兩個物件是否相等
  • finalize():如果物件沒有任何參考指向它,則會呼叫垃圾收集器(garbage collector)回收
  • getClass():取得此物件執行時期的類別
  • hashCode():雜湊值
  • notify():喚醒正在此物件監視上等待的單個線程。
  • notifyAll():喚醒等待此物件監視上等待的所有線程。
  • toString():回傳代表此物件的字串說明
  • wait():導致當前線程等待,直到另一個線程調用此物件的 notify() 方法或 notifyAll() 方法。
  • wait(long timeout):導致當前線程等待,直到另一個線程調用此物件的 notify() 方法或 notifyAll() 方法,或者已經過了指定的時間量。
  • wait(long timeout, int nanos):導致當前線程等待,直到另一個線程為此對象調用 notify() 方法或 notifyAll() 方法,或者某個其他線程中斷當前線程,或者已經過了指定的時間量。

4.1 toString()

Object 的 toString() 方法目的是傳回物件本身的描述,預設上 Object 的 toString() 方法會傳回以下的內容:


4.2 finalize()

在 Java 中建立的物件,如果沒有被任何的名稱參考,它將會被 JVM 回收,以釋放物件所佔據的資源,例如:

object1、object2、object4 三個物件有被名稱參考,而 object3 沒有被任何名稱參考,也就是不會有方式可以使用到 object3,JVM 發現到這樣一個物件時,會在適當的時候回收該物件。

在 JVM 回收物件之前,會執行物件的 finalize()方法,您可以在這個方法中撰寫一些物件被回收前的善後工作,然而要注意的是,JVM 何時會回收物件並無法預知,所以如果您有立即性必須馬上處理的工作,不可以依賴在 finalize()方法中完成,當您讓某個物件不再被參考,JVM 回收它的時間可能是在一分鐘、五分鐘或十分鐘之後回收物件,並不知道準確的時間點回收,所以您只可以在 finalize()中撰寫一些非即時必須處理的善後工作,例如留下操作簡單的記錄(log)之類,而不能放一些像是釋放資料庫連結的動作。

在物件導向程式中,物件往往在程式的各個角落被參考,何時該回收資源是件複雜且難以判斷的工作,自動回收資源是 Java 的垃圾收集( Garbage collection )機制,為的是讓開發人員不用費心於物件何時該釋放資源,必要的時候您可以建議 JVM 進行物件的回收,但也只能建議,因為 JVM 並不一定採納您的建議,當程式中有更高優先權的執行緒(Thread)在執行時,您的建議會被忽略。


範例:呼叫 toString() 與 finalize() 方法。


其他方法在後續文章中再介紹詳細用法。



五、多型(polymorphism)

如果類別與類別之間有繼承關係,則您可以用父類別宣告一個參考名稱,並讓其參考至子類別的物件實例,並以父類別上的公開介面來操作子類別上對應的公開方法,這是多型的基本操作方式。

更進一步的,您可以在父類別中事先規範子類別必須實作的方法,父類別中暫時無需實作,這樣的方法稱之為抽象方法,它的目的是先規範操作方法,並在執行時期可以操作各種子類別的物件實例。

目的:使用同一個變數可以操作多種空間

語法:父類別 物件參考變數 = new 子類別建構子方法()

多型的注意事項:

◉ 一個物件實例只有一個型別(被建構的型別)。然而變數是多型的,因為它們可指向有繼承關係的父子型別的物件實例。
◉ 所謂多型,泛指在具有繼承關係的架構下,單一的物件參考變數可以被宣告為多種型別。
◉ 多型也是一種覆寫機制的變化。
◉ 多型用於 non-static 方法

— 沒覆寫,執行父類別的方法。
— 有覆寫成功,則執行子類別已覆蓋的方法

◉ 多型用於 static 方法

— 表示欲使用父類別中同名的 static 方法。

◉ 多型用於屬性變數

— 表示欲使用父類別中同名的屬性變數


範例:測試多型

"Java is good!" 在執行時期會產生一個 String 實例代表,您可以使用 Object 所宣告的名稱 obj1 參考而不會有任何的錯誤,在 Java 中您可以讓父類別所宣告的參考名稱,參考至子類別的實例。

Object 上有 toString()、getClass()、hashCose()等方法,而 String 類別也繼承這些方法。由於擁有同樣的操作方法,因而您可以透過 obj1 參考名稱來操作 toString()、getClass()、hashCose()等方法。


obj1 名稱是 Object 型態,即使如此您仍可以正確操作 String 實例上擁有的共同方法,即 Object 上已規範的方法。您會覺得納悶!?為什麼第6行 obj1.toString() 顯示出的文字並不是如 3.1 toString() 章節中預設的輸出格式呢?那是因為 String 類別已經覆寫 toString() 方法,故呼叫時,父類別的方法會被遮蔽,所以會呼叫 String 類別中已覆寫的 toString() 方法。


像這樣透過 Object 型態的參考名稱來操作 String 子類別的物件實例,就是多型(Polymorphism)操作的一個實際例子。由於您所使用的是 Object 型態的介面,所以無法操作 String 子類別上自己定義的 toUpperCase()、toLowerCase() 等方法,Object 型態的名稱並不認識那些方法。

直接使用 toUpperCase() 會編譯錯誤


如果您要操作子類別上的特定方法時,您要先轉換操作的介面,例如:

這次使用的是 String 型態的物件參考變數,因此可以正確操作 toUpperCase() 方法了,結果會在螢幕上顯示 JAVA IS EVERYWHERE.


範例:繼承、覆寫與轉型



六、總結

抽象方法(abstract method)先規範操作方法,並在執行時期可以操作各種子類別的物件實例。

在 Java 中介面(interface)用來規範公開的操作介面,一個實作介面的某類別,必須實作介面中所有已規範的操作方法,一個類別可以實作多個介面,實現多重繼承。這表示一個類別可以身兼多個角色與職責。

所有類別都會繼承 java.lang.Object 類別,Object 類別是所有類別的根。

多型(polymorphism)使用父類別宣告一個參考名稱,並讓其參考至子類別的物件實例,並以父類別上的公開介面來操作子類別上對應的公開方法。





沒有留言:

張貼留言