精通 OOP:繼承、接口和抽像類實用指南
已發表: 2022-03-10據我所知,軟件開發領域的教育內容提供了理論和實踐信息的適當混合是不常見的。 如果我猜為什麼,我認為這是因為專注於理論的人傾向於進入教學,而專注於實踐信息的人傾向於使用特定的語言和工具解決特定問題而獲得報酬。
當然,這是一個廣泛的概括,但如果我們為了論證而簡單地接受它,那麼許多擔任教師角色的人(絕不是所有人)往往要么不擅長,要么完全沒有能力解釋與特定概念相關的實踐知識。
在本文中,我將盡力討論在大多數面向對象編程 (OOP) 語言中會發現的三種核心機制:繼承、接口(又名協議)和抽像類。 與其給你關於每種機制是什麼的技術性和復雜的口頭解釋,我會盡力關注它們的作用以及何時使用它們。
但是,在單獨討論它們之前,我想簡要討論一下給出理論上合理但實際上無用的解釋意味著什麼。 我希望您可以使用這些信息來幫助您篩選不同的教育資源,並避免在事情沒有意義時責備自己。
不同程度的認識
知道名字
知道某物的名稱可以說是最膚淺的認識形式。 事實上,一個名稱通常只有在許多人常用來指代同一事物和/或有助於描述事物的情況下才有用。 不幸的是,正如任何在該領域花費時間的人所發現的那樣,許多人為同一事物使用不同的名稱(例如接口和協議),為不同的事物使用相同的名稱(例如模塊和組件),或者對荒謬的點(例如Either Monad )。 歸根結底,名稱只是心理模型的指針(或引用),它們可能具有不同程度的有用性。
為了讓這個領域更難研究,我大膽猜測,對於大多數人來說,編寫代碼是(或至少是)一種非常獨特的體驗。 更複雜的是理解代碼如何最終編譯成機器語言,並在物理現實中表示為一系列隨時間變化的電脈衝。 即使人們能回憶起程序中使用的過程、概念和機制的名稱,也不能保證一個人為這些事情創建的心智模型與另一個人的模型是一致的。 更不用說它們是否客觀準確。
正是由於這些原因,加上我天生對行話沒有很好的記憶力,我認為名字是了解某事最不重要的方面。 這並不是說名字沒用,但我過去在項目中學習和使用了許多設計模式,只是幾個月甚至幾年後才知道常用的名字。
了解語言定義和類比
語言定義是描述新概念的自然起點。 但是,與名稱一樣,它們可能具有不同程度的有用性和相關性; 這在很大程度上取決於學習者的最終目標是什麼。 我在語言定義中看到的最常見的問題是假設知識通常以行話的形式出現。
例如,假設我要解釋線程與進程非常相似,只是線程佔用給定進程的相同地址空間。 對於已經熟悉進程和地址空間的人,我基本上已經說過線程可以與他們對進程的理解相關聯(即它們具有許多相同的特徵),但可以根據不同的特徵來區分它們。
對於不具備這種知識的人來說,我充其量是沒有任何意義的,最壞的情況是讓學習者在某種程度上因為不知道我認為他們應該知道的事情而感到不足。 公平地說,如果您的學習者真的應該擁有這樣的知識(例如教研究生或經驗豐富的開發人員),這是可以接受的,但我認為在任何入門級材料中這樣做是一個巨大的失敗。
當一個概念不同於學習者以前見過的任何其他東西時,通常很難對一個概念進行良好的口頭定義。 在這種情況下,對於教師來說,選擇一個普通人可能熟悉的類比是非常重要的,並且只要它傳達了該概念的許多相同品質,它也是相關的。
例如,對於軟件開發人員來說,了解軟件實體(程序的不同部分)緊耦合或鬆耦合時的含義至關重要。 在建造花園棚時,初級木匠可能會認為使用釘子而不是螺釘將其組裝起來更快更容易。 直到發生錯誤或花園棚設計的更改需要重建棚屋的一部分之前,情況都是如此。
在這一點上,使用釘子將花園棚的各個部分緊密連接在一起的決定使整個施工過程變得更加困難,可能更慢,並且用錘子提取釘子存在損壞結構的風險。 相反,螺釘可能需要一些額外的時間來組裝,但它們很容易拆卸,並且幾乎沒有損壞棚屋附近部分的風險。 這就是我所說的鬆散耦合。 當然,在某些情況下你真的只需要一顆釘子,但這個決定應該以批判性思維和經驗為指導。
正如我稍後將詳細討論的,將程序的各個部分連接在一起有不同的機制,它們提供了不同程度的耦合; 就像釘子和螺絲一樣。 雖然我的類比可能幫助你理解了這個至關重要的術語的含義,但我沒有告訴你如何在建造花園棚的範圍之外應用它。 這使我獲得了最重要的知識,也是深入理解任何研究領域中模糊和困難概念的關鍵; 儘管我們將堅持在本文中編寫代碼。
了解代碼
在我看來,嚴格來說,就軟件開發而言,了解一個概念最重要的形式來自能夠在工作應用程序代碼中使用它。 只需編寫大量代碼並解決許多不同的問題即可獲得這種形式的知識。 不需要包括行話名稱和口頭定義。
以我自己的經驗,我記得通過一個接口解決了與遠程數據庫和本地數據庫通信的問題(如果你還不知道,你很快就會知道這意味著什麼); 而不是需要顯式調用遠程和本地(甚至是測試數據庫)的客戶端(與接口對話的任何類)。 事實上,客戶端並不知道界面背後是什麼,所以無論它是在生產應用程序中運行還是在測試環境中運行,我都不需要更改它。 在我解決這個問題大約一年後,我遇到了術語“外觀模式”,並且在“存儲庫模式”這個術語之後不久,這兩個名稱都是人們用於前面描述的解決方案的名稱。
所有這些前言都是希望闡明一些在解釋諸如繼承、接口和抽像類等主題時最常出現的缺陷。 在這三種方法中,繼承可能是最容易使用和理解的一種。 根據我作為一名編程學生和一名教師的經驗,除非特別注意避免前面討論的錯誤,否則其他兩個幾乎總是對學習者來說是一個問題。 從這一點開始,我將盡我所能使這些主題盡可能簡單,但不會更簡單。
關於示例的註釋
我本人最精通Android移動應用程序開發,我將使用來自該平台的示例,以便在介紹Java語言特性的同時教您構建GUI應用程序。 但是,我不會詳細介紹這些示例,以使粗略了解 Java EE、Swing 或 JavaFX 的人無法理解這些示例。 我討論這些主題的最終目標是幫助您理解它們在解決幾乎任何類型的應用程序中的問題時的含義。
親愛的讀者,我還想警告你,有時我似乎對特定的詞及其定義過於哲學和迂腐。 這樣做的原因是,確實需要深刻的哲學基礎來理解具體(真實)與抽象(不如真實事物詳細)之間的區別。 這種理解適用於計算領域之外的許多事物,但對於任何軟件開發人員來說,掌握抽象的本質都特別重要。 無論如何,如果我的話讓你失望,代碼中的示例希望不會。
繼承與實現
在使用圖形用戶界面 (GUI) 構建應用程序時,繼承可以說是使快速構建應用程序成為可能的最重要的機制。
儘管稍後將討論使用繼承的好處鮮為人知,但主要好處是在類之間共享實現。 至少就本文的目的而言,“實施”一詞具有獨特的含義。 用英語給這個詞一個一般的定義,我可能會說實現某事,就是讓它成為現實。
為了給出一個特定於軟件開發的技術定義,我可以說實現一個軟件,就是編寫滿足該軟件要求的具體代碼行。 例如,假設我正在編寫一個sum 方法: private double sum(double first, double second){
private double sum(double first, double second){ //TODO: implement }
上面的代碼片段,即使我已經編寫了返回類型( double
)和指定參數( first, second
)和可用於調用所述方法的名稱( sum
)的方法聲明,它也有沒有被執行。 為了實現它,我們必須像這樣完成方法體:
private double sum(double first, double second){ return first + second; }
當然,第一個示例不會編譯,但我們會立即看到接口是一種我們可以編寫這些類型的未實現函數而不會出錯的方式。
Java中的繼承
據推測,如果您正在閱讀這篇文章,那麼您至少已經使用過extends
Java 關鍵字一次。 這個關鍵字的機制很簡單,最常使用示例來描述不同種類的動物或幾何形狀; Dog
和Cat
擴展Animal
,依此類推。 我假設我不需要向您解釋基本的類型理論,所以讓我們通過extends
關鍵字直接了解 Java 中繼承的主要好處。
用 Java 構建一個基於控制台的“Hello World”應用程序非常簡單。 假設您擁有 Java 編譯器 ( javac ) 和運行時環境 ( jre ),您可以編寫一個包含如下主函數的類:
public class JavaApp{ public static void main(String []args){ System.out.println("Hello World"); } }
在幾乎所有主要平台(Android、企業/Web、桌面)上用 Java 構建 GUI 應用程序,在 IDE 的幫助下生成新應用程序的框架/樣板代碼,也相對容易,這要歸功於extends
關鍵字。
假設我們有一個名為activity_main.xml
的 XML 佈局(我們通常在 Android 中通過佈局文件以聲明方式構建用戶界面),其中包含一個名為tvDisplay
的TextView
(如文本標籤):
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </FrameLayout>
另外,假設我們希望tvDisplay
說“Hello World!” 為此,我們只需要編寫一個使用extends
關鍵字從Activity
類繼承的類:
import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((TextView)findViewById(R.id.tvDisplay)).setText("Hello World"); }
通過快速查看其源代碼,可以最好地了解繼承Activity
類的實現的效果。 如果一個人需要實現與系統交互所需的 8000 多行代碼中的一小部分,我非常懷疑 Android 是否會成為主導的移動平台,只是為了生成一個帶有一些文本的簡單窗口。 繼承使我們不必從頭開始重建Android 框架或您碰巧使用的任何平台。
繼承可用於抽象
就它可以用於跨類共享實現而言,繼承相對容易理解。 然而,還有另一種重要的方式可以使用繼承,它在概念上與我們將很快討論的接口和抽像類相關。
如果您願意,請在接下來的一小段時間裡假設在最一般意義上使用的抽像是對事物的不太詳細的表示。 與其用冗長的哲學定義來限定它,我將嘗試指出抽像在日常生活中是如何工作的,然後不久就軟件開發方面明確討論它們。
假設您正在澳大利亞旅行,並且您知道您所訪問的地區是內陸大班蛇密度特別高的地區(它們顯然非常有毒)。 您決定諮詢維基百科,通過查看圖像和其他信息來了解更多關於它們的信息。 通過這樣做,你現在敏銳地意識到了一種你以前從未見過的特定種類的蛇。
抽象、想法、模型或任何你想稱呼它們的東西,都是對事物的不太詳細的表示。 重要的是它們沒有真實的東西那麼詳細,因為真正的蛇會咬你; 維基百科頁面上的圖像通常不會。 抽像也很重要,因為計算機和人腦存儲、交流和處理信息的能力都是有限的。 有足夠的細節以實用的方式使用這些信息,而不佔用太多的內存空間,這使得計算機和人腦能夠解決問題。
為了將其與繼承聯繫起來,我在這裡討論的所有三個主要主題都可以用作抽像或抽像機制。 假設在我們的“Hello World”應用程序的佈局文件中,我們決定添加一個ImageView
、 Button
和ImageButton
:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageButton android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageView android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
還假設我們的 Activity 已經實現View.OnClickListener
來處理點擊:
public class MainActivity extends Activity implements View.OnClickListener { private Button b; private ImageButton ib; private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... b = findViewById(R.id.imvDisplay).setOnClickListener(this); ib = findViewById(R.id.btnDisplay).setOnClickListener(this); iv = findViewById(R.id.imbDisplay).setOnClickListener(this); } @Override public void onClick(View view) { final int id = view.getId(); //handle click based on id... } }
這裡的關鍵原則是Button
、 ImageButton
和ImageView
繼承自View
類。 結果是這個函數onClick
可以通過將不同的(儘管是分層相關的)UI 元素引用為它們不太詳細的父類來接收點擊事件。 這比必須編寫一個獨特的方法來處理Android 平台上的每種小部件(更不用說自定義小部件)要方便得多。
接口和抽象
即使您理解我選擇它的原因,您也可能發現前面的代碼示例有點乏味。 能夠跨類的層次結構共享實現非常有用,我認為這是繼承的主要用途。 至於允許我們將具有共同父類的一組類視為類型相同(即作為父類),繼承這一特性的用途有限。
受限於,我說的是子類必須在同一個類層次結構中才能被引用,或者稱為父類。 換句話說,繼承是一種非常嚴格的抽像機制。 事實上,如果我假設抽像是在不同細節(或信息)級別之間移動的頻譜,我可能會說繼承是 Java 中抽象程度最低的抽像機制。
在繼續討論接口之前,我想提一下,從Java 8開始,接口中添加了兩個稱為默認方法和靜態方法的特性。 我最終會討論它們,但目前我希望我們假裝它們不存在。 這是為了讓我更容易解釋使用接口的主要目的,它最初是,並且可以說仍然是Java 中最抽象的抽像機制。
更少的細節意味著更多的自由
在關於繼承的部分中,我給出了實現這個詞的定義,這是為了與我們現在將要討論的另一個術語進行對比。 需要明確的是,我不在乎這些詞本身,也不在乎你是否同意它們的用法; 只是您了解它們在概念上指向的內容。
繼承主要是一種在一組類之間共享實現的工具,而我們可以說接口主要是一種在一組類之間共享行為的機制。 在這個意義上使用的行為實際上只是抽象方法的一個非技術詞。 抽象方法是一種實際上不包含方法體的方法:
public interface OnClickListener { void onClick(View v); }
對我和我輔導過的一些人來說,在第一次看到一個接口之後,自然的反應是想知道隻共享一個返回類型、方法名稱和參數列表可能會有什麼用處。 從表面上看,這似乎是一種為自己或其他人可能正在編寫implements
接口的類創建額外工作的好方法。 答案是,接口非常適合您希望一組類以相同方式運行的情況(即它們擁有相同的公共抽象方法),但您希望它們以不同的方式實現該行為。
舉一個簡單但相關的例子,Android 平台有兩個類,主要用於創建和管理部分用戶界面的業務: Activity
和Fragment
。 因此,這些類通常需要偵聽單擊小部件(或以其他方式與用戶交互)時彈出的事件。 為了論證的緣故,讓我們花一點時間來理解為什麼繼承幾乎永遠不會解決這樣的問題:
public class OnClickManager { public void onClick(View view){ //Wait a minute... Activities and Fragments almost never //handle click events exactly the same way... } }
讓我們的活動和片段繼承自OnClickManager
會使我們無法以不同的方式處理事件,而且更糟糕的是,如果我們願意,我們甚至無法做到這一點。 Activity和Fragment都已經擴展了一個父類,Java 不允許有多個父類。 所以我們的問題是我們希望一組類以相同的方式表現,但我們必須在類如何實現該行為方面具有靈活性。 這讓我們回到前面的View.OnClickListener
示例:
public interface OnClickListener { void onClick(View v); }
這是實際的源代碼(嵌套在View
類中),這幾行代碼允許我們確保跨不同小部件(視圖)和 UI 控制器(活動、片段等)的行為一致。
抽象促進松耦合
希望我已經回答了關於 Java 中為什麼存在接口的一般性問題。 在許多其他語言中。 從一個角度來看,它們只是在類之間共享代碼的一種方式,但是為了允許不同的實現,它們故意不那麼詳細。 但是正如繼承可以用作共享代碼和抽象的機制(儘管對類層次結構有限制),接口提供了一種更靈活的抽像機制。
在本文的前面部分,我通過類比使用釘子和螺釘構建某種結構的區別來介紹松/緊耦合的主題。 回顧一下,基本思想是,在可能發生更改現有結構(可能是由於修復錯誤、設計更改等)的情況下,您將需要使用螺釘。 當您只需要將結構的一部分固定在一起並且並不特別擔心在不久的將來將它們拆開時,可以使用釘子。
釘子和螺釘類似於類之間的具體和抽象引用(術語依賴項也適用)。 為了避免混淆,以下示例將說明我的意思:
class Client { private Validator validator; private INetworkAdapter networkAdapter; void sendNetworkRequest(String input){ if (validator.validateInput(input)) { try { networkAdapter.sendRequest(input); } catch (IOException e){ //handle exception } } } } class Validator { //...validation logic boolean validateInput(String input){ boolean isValid = true; //...change isValid to false based on validation logic return isValid; } } interface INetworkAdapter { //... void sendRequest(String input) throws IOException; }
在這裡,我們有一個名為Client
的類,它擁有兩種引用。 請注意,假設Client
與創建它的引用沒有任何關係(它確實不應該),它與任何特定網絡適配器的實現細節是分離的。
這種鬆散耦合有幾個重要的含義。 對於初學者,我可以在絕對隔離INetworkAdapter
的任何實現的情況下構建Client
。 想像一下,您在一個由兩個開發人員組成的團隊中工作; 一個構建前端,一個構建後端。 只要兩個開發人員都知道將他們各自的類耦合在一起的接口,他們就可以幾乎獨立地繼續工作。
其次,如果我要告訴您兩個開發人員都可以驗證他們各自的實現是否正常運行,並且彼此獨立於彼此的進度,該怎麼辦? 使用接口很容易; 只需構建一個implements
適當接口的Test Double :
class FakeNetworkAdapter implements INetworkAdapter { public boolean throwError = false; @Override public void sendRequest(String input) throws IOException { if (throwError) throw new IOException("Test Exception"); } }
原則上,可以觀察到使用抽象引用打開了增加模塊化、可測試性和一些非常強大的設計模式的大門,例如外觀模式、觀察者模式等。 它們還可以讓開發人員在基於行為( Program To An Interface )設計系統的不同部分之間找到一個快樂的平衡,而不會陷入實現細節的困境。
關於抽象的最後一點
抽象與具體事物的存在方式不同。 這反映在 Java 編程語言中,抽像類和接口可能不會被實例化。
例如,這絕對不會編譯:
public class Main extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { //ERROR x2: Foo f = new Foo(); Bar b = new Bar() } private abstract class Foo{} private interface Bar{} }
事實上,期望未實現的接口或抽像類在運行時起作用的想法與期望 UPS 統一在交付包裹時浮動一樣有意義。 抽象背後必須有具體的東西才能有用; 即使調用類不需要知道抽象引用背後的實際內容。
抽像類:把它們放在一起
如果你已經做到了這一步,那麼我很高興地告訴你,我沒有更多的哲學切線或行話要翻譯了。 簡單地說,抽像類是一種在一組類之間共享實現和行為的機制。 現在,我會馬上承認,我發現自己並不經常使用抽像類。 即便如此,我希望在本節結束時,您將確切知道何時需要它們。
鍛煉日誌案例研究
用 Java 構建 Android 應用程序大約一年後,我從頭開始重新構建我的第一個 Android 應用程序。 第一個版本是您期望自學成才的開發人員幾乎沒有指導的那種可怕的大量代碼。 當我想添加新功能時,很明顯,我專門用釘子構建的緊密耦合結構已經無法維護,我必須完全重建它。
該應用程序是一個鍛煉日誌,旨在輕鬆記錄您的鍛煉,並能夠將過去鍛煉的數據輸出為文本或圖像文件。 在沒有深入細節的情況下,我構建了應用程序的數據模型,以便有一個Workout
對象,該對象由一組Exercise
對象(以及與本討論無關的其他字段)組成。
當我實現將鍛煉數據輸出到某種視覺媒體的功能時,我意識到我必須處理一個問題:不同類型的鍛煉需要不同類型的文本輸出。
為了給你一個粗略的想法,我想根據練習的類型來改變輸出,如下所示:
- 槓鈴:10 REPS @ 100 LBS
- 啞鈴:10 REPS @ 50 LBS x2
- 體重:10 REPS @ 體重
- 體重 +:10 REPS @ 體重 + 45 磅
- 定時:60 秒 @ 100 磅
在我繼續之前,請注意還有其他類型(工作可能會變得複雜),並且我將展示的代碼已被修剪並更改為很好地適合一篇文章。
按照我之前的定義,編寫抽像類的目標是實現抽像類中所有子類共享的所有內容(甚至是變量和常量等狀態)。 然後,對於在所述子類中發生變化的任何內容,創建一個抽象方法:
abstract class Exercise { private final String type; protected final String name; protected final int[] repetitionsOrTime; protected final double[] weight; protected static final String POUNDS = "LBS"; protected static final String SECONDS = "SEC "; protected static final String REPETITIONS = "REPS "; public Exercise(String type, String name, int[] repetitionsOrTime, double[] weight) { this.type = type; this.name = name; this.repetitionsOrTime = repetitionsOrTime; this.weight = weight; } public String getFormattedOutput(){ StringBuilder sb = new StringBuilder(); sb.append(name); sb.append("\n"); getSetData(sb); sb.append("\n"); return sb.toString(); } /** * Append data appropriately based on Exercise type * @param sb - StringBuilder to Append data to */ protected abstract void getSetData(StringBuilder sb); //...Getters }
我可能會說顯而易見的,但是如果您對抽像類中應該或不應該實現什麼有任何疑問,關鍵是查看在所有子類中重複的實現的任何部分。
現在我們已經確定了所有練習中的共同點,我們可以開始為每種字符串輸出創建具有特化的子類:
槓鈴練習:
class BarbellExercise extends Exercise { public BarbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append("\n"); } } }
啞鈴練習:
class DumbbellExercise extends Exercise { private static final String TIMES_TWO = "x2"; public DumbbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append(TIMES_TWO); sb.append("\n"); } } }
體重鍛煉:
class BodyweightExercise extends Exercise { private static final String BODYWEIGHT = "Bodyweight"; public BodyweightExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(BODYWEIGHT); sb.append("\n"); } } }
我確信一些精明的讀者會發現可以以更有效的方式抽像出來的東西,但是這個例子的目的(已經從原始來源簡化)是為了展示一般的方法。 當然,沒有可以執行的東西,沒有任何編程文章是完整的。 如果您想對其進行測試,可以使用幾個在線 Java 編譯器來運行此代碼(除非您已經有 IDE):
public class Main { public static void main(String[] args) { //Note: I actually used another nested class called a "Set" instead of an Array //to represent each Set of an Exercise. int[] reps = {10, 10, 8}; double[] weight = {70.0, 70.0, 70.0}; Exercise e1 = new BarbellExercise( "Barbell", "Barbell Bench Press", reps, weight ); Exercise e2 = new DumbbellExercise( "Dumbbell", "Dumbbell Bench Press", reps, weight ); Exercise e3 = new BodyweightExercise( "Bodyweight", "Push Up", reps, weight ); System.out.println( e1.getFormattedOutput() + e2.getFormattedOutput() + e3.getFormattedOutput() ); } }
Executing this toy application yields the following output: Barbell Bench Press
10 REPS @ 70.0LBS 10 REPS @ 70.0LBS 8 REPS @ 70.0LBS Dumbbell Bench Press 10 REPS @ 70.0LBSx2 10 REPS @ 70.0LBSx2 8 REPS @ 70.0LBSx2 Push Up 10 REPS @ Bodyweight 10 REPS @ Bodyweight 8 REPS @ Bodyweight
Further Considerations
Earlier, I mentioned that there are two features of Java interfaces (as of Java 8) which are decidedly geared towards sharing implementation , as opposed to behavior . These features are known as Default Methods and Static Methods .
I have decided not to go into detail on these features for the reason that they are most typically used in mature and/or large code bases where a given interface has many inheritors. Despite the fact that this is meant to be an introductory article, and I still encourage you to take a look at these features eventually, even though I am confident that you will not need to worry about them just yet.
I would also like to mention that there are other ways to share implementation across a set of classes (or even static methods ) in a Java application that does not require inheritance or abstraction at all. For example, suppose you have some implementation which you expect to use in a variety of different classes, but does not necessarily make sense to share via inheritance . A common pattern in Java is to write what is known as a Utility class, which is a simple class
containing the requisite implementation in a static method :
public class TimeConverterUtil { /** * Accepts an hour (0-23) and minute (0-59), then attempts to format them into an appropriate * format such as 12, 30 -> 12:30 pm */ public static String convertTime (int hour, int minute){ String unformattedTime = Integer.toString(hour) + ":" + Integer.toString(minute); DateFormat f1 = new SimpleDateFormat("HH:mm"); Date d = null; try { d = f1.parse(unformattedTime); } catch (ParseException e) { e.printStackTrace(); } DateFormat f2 = new SimpleDateFormat("h:mm a"); return f2.format(d).toLowerCase(); } }
Using this static method in an external class (or another static method ) looks like this:
public class Main { public static void main(String[] args){ //... String time = TimeConverterUtil.convertTime(12, 30); //... } }
備忘單
We have covered a lot of ground in this article, so I would like to spend a moment summarizing the three main mechanisms based on what problems they solve. Since you should possess a sufficient understanding of the terms and ideas I have either introduced or redefined for the purposes of this article, I will keep the summaries brief.
I Want A Set Of Child Classes To Share Implementation
Classic inheritance , which requires a child class to inherit from a parent class , is a very simple mechanism for sharing implementation across a set of classes. An easy way to decide if some implementation should be pulled into a parent class , is to see whether it is repeated in a number of different classes line for line. The acronym DRY ( Don't Repeat Yourself ) is a good mnemonic device to watch out for this situation.
While coupling child classes together with a common parent class can present some limitations, a side benefit is that they can all be referenced as the parent class , which provides a limited degree of abstraction .
I Want A Set Of Classes To Share Behavior
Sometimes, you want a set of classes to be capable of possessing certain abstract methods (referred to as behavior ), but you do not expect the implementation of that behavior to be repeated across inheritors.
By definition, Java interfaces may not contain any implementation (except for Default and Static Methods ), but any class which implements an interface , must supply an implementation for all abstract methods, otherwise, the code will not compile. This provides a healthy measure of flexibility and restriction on what is actually shared and does not require the inheritors to be of the same class hierarchy .
I Want A Set Of Child Classes To Share Behavior And Implementation
Although I do not find myself using abstract classes all over the place, they are perfect for situations when you require a mechanism for sharing both behavior and implementation across a set of classes. Anything which will be repeated across inheritors may be implemented directly in the abstract class
, and anything which requires flexibility may be specified as an abstract method .