CS61B資料結構及演算法筆記:(四)接口繼承
列表是一個抽象的概念,可以透過陣列或是節點的方式來完成,但其實這兩者非常相似;假設今天要建立一個新的類別 WordUtils,類別內的方法 longest 能從存放數個字串的列表中,挑選出長度最長的字串,並且希望可以同時支援這兩種列表。
其中一個做法是同時寫入兩個同名的方法(method),差別在於方法的參數一個為 AList 而另一個為 SLList,Java 會根據實際上輸入的物件屬於哪一種列表,呼叫相對應的方法,而這個支援同時存在同名的方法又稱「多載(overloading)」。
這個做法雖然可行,不過可以發現兩個方法的內容幾乎一模一樣,導致代碼變得更冗長,而且如果其中一個方法發生問題,必須要同時修正另一個同名的方法,因此難以維護。
如只想留下一個方法卻又可以同時支援兩種列表,就需要使用 Java 接口(interface)的概念。首先完成兩種列表的類別分別是 AList 和 SLList,接著建立一個接口 List61B。以 List61B 為例,這個接口只會描述一個列表,無論是 AList、SLList、DLList…等等各種列表,應該要可以做的事情,而不需說明如何完成這些事情。
緊接著,在各個列表的類別後加上 implements 的關鍵字,代表這些列表都是 List61B,並且在該類別內實現每一個寫在接口內未實現的方法。此時在 WordUtils 內只需要一個 longest 方法,並將特定的列表改寫為 List61B,代表無論實際上用的是 AList 或是 SLList,都可以用接口下的方法取出最長的字串。
接口和類別的關係如圖一所示,從中可以觀察到兩種列表都在接口底下,因此也可以說 List61B 是 SLList 的父類別(superclass),或是 SLList 是 List61B 的子類別(subclass);而 SLList 又以 implements 的關鍵字實現了接口 List61B,代表 SLList 這個類別告訴所有人我實現了 List61B 內的所有方法,又可稱 SLList 覆寫(overriding)了這些定義在接口內方法。
繼承(Inheritance)
使用 implements 這個關鍵字時,等同於一個子類別從所有父類別,包含父類別的父類別,繼承了所有方法,也可稱接口繼承(interface inheritance)。
雖然接口不需要實現任何方法,不過可以透過關鍵字 default 來定義所有繼承這個接口的子類別,都預設可以實現這個方法,舉例來說,預設所有 List61B 的子類別,即使在子類別內沒有實現 print(),都可以使用印出所有列表內元素的方法 print() ,又稱實現繼承(implement inheritance)。
雖然在接口內並沒有定義如何實現一個列表,不過因為規定所有子類別必須要覆寫 size() 和 get() 這兩個方法,因此在接口內可以相信 print() 這個方法一定可以在子類別被實現。然而,這個預設的方法在 AList 和 SLList 的效率表現上卻截然不同,單向串列在呼叫 get() 這個方法時會遍歷整個列表,運算的時間會隨著列表長度增加而增加。因此,可以在 SLList 的類別內,使用@Override 這個標籤(tag),覆寫父類別內預設的方法,改寫為較有效率的方式,以減少 SLList 在呼叫 print() 這個方法時所需的時間。
方法選擇(Method Selection)
在 Java 的規則中,宣告一個變數時需要指定變數的靜態類型,例如宣告變數 x 為 int,此時變數 x 只能存放整數,對其存放不同類型如:
x = "horse";
Java 會顯示編譯錯誤,原因是已經指定變數只能存放整數,無法存放字串。當有了繼承關係以後,Java 允許宣告為父類別的變數並存放子類別,因此,考慮宣告一個變數 s1 為 List61B,並以 SLList 實例化:
List61B s1 = new SLList();
當 s1 使用 print() 方法時,直觀上會使用 SLList 內覆寫預設方法的方法,而非使用在接口內預設的 print(),否則就失去覆寫的意義。不過實際上 Java 判斷的方式是,在宣告變數時,先定義變數 s1 的指標(pointer)永遠只能指向 List61B,換句話說,這 64 位元的記憶體空間,只能存放 List61B 或是其子類別,此時變數 s1 的靜態類型(static type)為 List61B;再者,當以 new 關鍵字實例化 s1 這個變數時,實際上會將指標指向 SLList,此時變數 s1 的動態類型(dynamic type)為 SLList,如圖所示。
此時,Java 的規則是當動態類型覆寫了(靜態類型內的) print() 方法時,Java 會選擇使用動態類型內的方法。這項規則並不會發生在多載的情況上,即使兩個多載的方法內的參數類別互為繼承關係,Java 還是會選擇靜態類型內的方法。
除了接口可以繼承以外,接下來會在下一個章節介紹如何繼承一個類別。