Java 基礎夯實1:細談抽象類別和介面,夯實細談
讀完本文你將瞭解:
背景介紹
什麼是抽象類別和介面
特點與區別
如何選擇
抽象與多態
總結
文章出自:安卓進階學習指南
主要貢獻者:
Milo
Struggle
shixinzhang
背景介紹
大家好,這篇文章是 《安卓進階技能樹計劃》 的第一部分 《Java 基礎系列》 的第一篇。
距離上一篇預告 《Java 基礎夯實系列上線預告》 過去了很久,之所以這麼慢,是因為我們做這個活動,除了要保證知識點的全面、完整,還想要讓每一篇文章都有自己的思考,儘可能的將知識點與實踐結合,努力讓讀者讀了有所收穫。每位小夥伴都有工作在身,每個知識點都需要經過思考、學習、寫作、提交、審核、修改、編輯、發布等多個過程,所以整體下來時間就會慢一些,這裡先向各位道歉。
《Java 基礎系列》初步整理大概有 12 篇,主要內容為。:
抽象類別和介面
內部類
修飾符
裝箱拆箱
註解
反射
泛型
異常
集合
IO
字串
其他
第一篇我們來聊聊抽象類別和介面。
“抽象類別和介面”聽起來是非常普遍的東西,有些朋友會覺得:這個太基礎了吧,有啥好說的,你又來糊弄我。
事實上我在面試中不僅一次被問到相關的問題:
抽象類別和介面之間的區別?
什麼時候建立抽象類別?什麼時候建立介面?
設計架構時該如何選擇?
我比較喜歡這樣的問題,答案可深可淺,體現了我們對日常工作的思考。
我們什麼時候會建立一個抽象類別?什麼時候會建立一個介面呢?當轉換一下思維,不僅僅為了完成功能,而是要保證整個項目架構的穩定靈活可擴充性,你會如何選擇呢?
這篇文章我們努力回答這些問題,也希望你可以說出你的答案。
什麼是抽象類別和介面
抽象方法 即使用 abstract
關鍵字修飾,僅有聲明沒有方法體的方法。
public abstract void f(); //沒有內容
抽象類別 即包含抽象方法的類。
如果一個類包含一個或者多個抽象方法,該類必須被限定為抽象的。抽象類別可以不包含抽象方法。
public abstract class BaseActivity { private final String TAG = this.getClass().getSimpleName(); //抽象類別可以有成員 void log(String msg){ //抽象類別可以有具體方法 System.out.println(msg); }// abstract void initView(); //抽象類別也可以沒有抽象方法}
介面 是抽象類別的一種特殊形式,使用 interface
修飾。
public interface OnClickListener { void onClick(View v);}
特點與區別抽象類別的特點
抽象類別的初衷是“抽象”,即規定這個類“是什麼”,具體的實現暫不確定,是不完整的,因此不允許直接建立執行個體。
抽象類別是由子類具有相同的一類特徵抽象而來,也可以說是其基類或者父類
抽象方法必須為 public 或者 protected(因為如果為 private,則不能被子類繼承,子類便無法實現該方法),預設情況下預設為 public
抽象類別不能用來建立對象
抽象方法必須由子類來實現
如果一個類繼承於一個抽象類別,則子類必須實現父類的抽象方法,如果子類沒有實現父類的抽象方法,則必須將子類也定義為抽象類別
抽象類別還是很有用的重構工具,因為它們使得我們可以很容易地將公用方法沿著繼承階層向上移動
介面的特點
Java 為了保證資料安全性是不能多繼承的,也就是一個類只有一個父類。
但是介面不同,一個類可以同時實現多個介面,不管這些介面之間有沒有關係,所以介面彌補了抽象類別不能多繼承的缺陷。
介面是抽象類別的延伸,它可以定義沒有方法體的方法,要求實現者去實現。
介面的所有方法存取權限自動被聲明為 public
介面中可以定義“成員變數”,會自動變為 public static final 修飾的**靜態常量**
實現介面的非抽象類別必須實現介面中所有方法,抽象類別可以不用全部實現
介面不能建立對象,但可以申明一個介面變數,方便調用
完全解耦,可以編寫可複用性更好的代碼
栗子
前面說了太多,我們直接上代碼。
假設我們新開始一個項目,需要寫大量的 Activity,這些 Activity 會有一些通用的屬性和方法,於是我們會建立一個基類,把這些通用的方法放進去:
public class BaseActivity extends Activity { private final String TAG = this.getClass().getSimpleName(); void toast(String msg) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); } //其他重複的工作,比如設定標題列、沈浸式狀態列、檢測網路狀態等等}
這時 BaseActivity
是一個基類,它的作用就是:封裝重複的內容。
寫著寫著,我們發現有的同事代碼寫的太爛了,一個方法裡幾百行代碼,看著太痛苦。於是我們就本著“職責分離”的原則,在 BaseActivity
裡建立了一些抽象方法,要求子類必須實現:
public abstract class BaseActivity extends Activity { private final String TAG = this.getClass().getSimpleName(); @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(getContentViewLayoutId()); initView(); //這裡初始化布局 loadData(); //這裡載入資料 } /** * 需要子類實現的方法 * @return */ protected abstract int getContentViewLayoutId(); protected abstract void initView(); protected abstract void loadData(); void toast(String msg) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); }}
定義的抽象方法存取權限修飾符可以是 public
protected
和 default
,但不能是 private
,因為這樣子類就無法實現了。
這時 BaseActivity
因為有了抽象方法,變成了一個抽象類別。它的作用就是:定義規範,強制子類符合標準;如果有調用抽象方法,也會制定執行順序的規則。
繼承 BaseActivity
的類只要實現這些方法,同時為父類提供需要的內容,就可以和父類一樣保證代碼的整潔性。
public class MainActivity extends BaseActivity{ private TextView mTitleTv; @Override protected int getContentViewLayoutId() { return R.layout.activity_main; } @Override void initView() { mTitleTv = (TextView) findViewById(R.id.main_title_tv); mTitleTv.setOnClickListener(this); } @Override protected void loadData() { //這裡載入資料 }}
以後如果發現有某些功能在不同 Activity 中重複出現的次數比較多,就可以把這個功能的實現提到 BaseActivity
中。但是注意不要輕易添加抽象方法,因為這會影響到之前的子類。
項目寫著寫著,發現很多頁面都有根據定位資訊改變而重新請求資料的情況,為了方便管理,再把這樣的代碼放到 BaseActivity
? 也可以,但是這樣一來,那些不需要定位相關的代碼不也被“汙染”了麼,而且冗餘邏輯太多 BaseActivity
不也成了大雜燴了麼。
我們想要把位置相關的放到另一個類,但是 Java 只有單繼承,這時就可以使用介面了。
我們建立一個介面表示對地理位置的監聽:
interface OnLocationChangeListener { void onLocationUpdate(String locationInfo);}
介面預設是 public,不能使用其他修飾符。
然後在一個位置觀察者裡持有這個介面的引用:
public class LocationObserver { List<OnLocationChangeListener> mListeners; public LocationObserver setListeners(final List<OnLocationChangeListener> listeners) { mListeners = listeners; return this; } public List<OnLocationChangeListener> getListeners() { return mListeners; } public void notify(String locationInfo) { if (mListeners != null) { for (OnLocationChangeListener listener : mListeners) { listener.onLocationUpdate(locationInfo); } } } interface OnLocationChangeListener { void onLocationUpdate(String locationInfo); }}
這樣我們在需要定位的頁面裡實現這個介面:
public class MainActivity extends BaseActivity implements View.OnClickListener, LocationObserver.OnLocationChangeListener { private TextView mTitleTv; @Override protected int getContentViewLayoutId() { return R.layout.activity_main; } @Override public void onClick(final View v) { int id = v.getId(); if (id == R.id.main_title_tv) { toast("你點擊了 title"); } } @Override void initView() { mTitleTv = (TextView) findViewById(R.id.main_title_tv); mTitleTv.setOnClickListener(this); } @Override protected void loadData() { //這裡載入資料 } @Override public void onLocationUpdate(final String locationInfo) { mTitleTv.setText("現在位置是:" + locationInfo); }}
這樣 MainActivity
就具有了監聽位置改變的能力。
如果 MainActivity
中需要添加其他功能,可以再建立對應的介面,然後予以實現。
小結
通過上面的代碼例子,我們可以很清晰地瞭解下面這張圖總結的內容。
圖片來自:http://www.jianshu.com/p/8f0a7e22bb8c
我們可以瞭解到抽象類別和介面的這些不同:
抽象層次不同
跨域不同
抽象類別所跨域的是具有相似特點的類,而介面卻可以跨域不同的類
抽象類別所體現的是一種繼承關係,考慮的是子類與父類本質**“是不是”**同一類的關係
而介面並不要求實現的類與介面是同一本質,它們之間只存在**“有沒有這個能力”**的關係
設計層次不同
如何選擇
現在我們知道了,抽象類別定義了“是什麼”,可以有非抽象的屬性和方法;介面是更純的抽象類別,在 Java 中可以實現多個介面,因此介面表示“具有什麼能力”。
在進行選擇時,可以參考以下幾點:
若使用介面,我們可以同時獲得抽象類別以及介面的好處
所以假如想建立的基類沒有任何方法定義或者成員變數,那麼無論如何都願意使用介面,而不要選擇抽象類別
如果事Crowdsourced Security Testing道某種東西會成為基礎類,那麼第一個選擇就是把它變成一個介面
只有在必須使用方法定義或者成員變數的時候,才應考慮採用抽象類別
此外使用介面最重要的一個原因:實現介面可以使一個類向上轉型至多個基礎類。
比如 Serializable
和 Cloneable
這樣常見的介面,一個類實現後就表示有這些能力,它可以被當做 Serializable
和 Cloneable
進行處理。
推薦介面和抽象類別同時使用,這樣既保證了資料的安全性又可以實現多繼承。
抽象與多態
俗話說:“做事留一線,日後好相見”。
程式開發也一樣,它是一個不斷遞增或者累積的過程,不可能一次做到完美,所以我們要儘可能地給後面修改留有餘地,而這就需要我們使用傳說中“物件導向的三個特徵” --- 繼承、封裝、多態。
不管使用抽象類別還是介面,歸根接地還是儘可能地職責分離,把業務抽象,也就是“面向介面編程”。
面向介面編程
日常生活裡與人約定時,一般不要說得太具體。就好比別人問我們什麼時候有空,回一句“大約在冬季” 一定比 “這周六中午” 靈活一點,誰知道這周六會不會突然有什麼變故。
我們在寫代碼時追求的是“以不變應萬變”,在需求變更時,儘可能少地修改代碼就可以實現。
而這,就需要模組之間依賴時,最好都只依賴對方給的抽象介面,而不是具體實現。
在設計模式裡這就是“依賴倒置原則”,依賴倒置有三種方式來實現:
通過建構函式傳遞依賴對象
通過 setter 方法傳遞依賴對象
介面聲明實現依賴對象,也叫介面注入
可以看到,“面向介面編程”說的“介面”也包括抽象類別,其實說的是基類,越簡單越好。
多態
多態指的是編譯期只知道是個人,具體是什麼樣的人需要在運行時能確定,同樣的參數有可能會有不同的實現。
通過抽象建立規範,在運行時替換成具體的對象,保證系統的擴充性、靈活性。
實現多態主要有以下三種方式:
介面實現
繼承父類重寫方法
同一類中進行方法重載
不論哪種實現方式,調用者持有的都是基類,不同的實現在他看來都是基類,使用時也當基類用。
這就是“向上轉型”,即:子類在被調用過程中由繼承關係的下方轉變成上面的角色。
向上轉型是能力減少的過程,編譯器可以幫我們實現;但 “向下轉型”是能力變強的過程,需要進行強轉。
以上面的代碼為例:
public class LocationObserver { List<OnLocationChangeListener> mListeners; public LocationObserver setListeners(final List<OnLocationChangeListener> listeners) { mListeners = listeners; return this; } public List<OnLocationChangeListener> getListeners() { return mListeners; } public void notify(String locationInfo) { if (mListeners != null) { for (OnLocationChangeListener listener : mListeners) { listener.onLocationUpdate(locationInfo); } } }}
LocationObserver
持有的是 OnLocationChangeListener
的引用,不管運行時傳入的是 MainActivity 還是其他 Activity,只要實現了這個介面,就可以被調用實現的方法。
在編譯期就知道要調用的是哪個方法,稱為“前期綁定”(又稱“靜態繫結”),由編譯器和串連程式實現。
在運行期調用正確的方法,這個過程稱為“動態綁定”,要實現動態綁定,就要有一種機制在運行期時可以根據對象的類型調用恰當的方法。這種機制是由虛擬機器實現的, invokevirtual
指令會把常量池中的類方法符號引用解析到不同的引用上,這個過程叫做“動態指派”,具體的實現過程我們暫不討論。
繼承和組合
儘管繼承在學習 OOP 的過程中得到了大量的強調,但並不意味著應該儘可能地到處使用它。
相反,使用它時要特別謹慎,因為繼承一個類,意味著你需要接受他的一切,不管貧窮富貴生老病死,你都得接受他,你能做到嗎?
一般人都無法做到白頭偕老,所以只有在清楚知道需要繼承所有方法的前提下,才可考慮它。
有一種取代繼承的方式是 “組合”。
組合就是通過持有一個類的引用來擁有他的一切,而不是繼承,在需要調用他的方法時傳入引用,然後調用,否則就清除引用。
組合比繼承靈活在於關係更松一些,繼承表示的是“is-a” 關係,比較強;而組合則是 "has-a" 關係。
為判斷自己到底應該選用合成還是繼承,一個最簡單的辦法就是考慮是否需要從新類向上轉型回基礎類。
假如的確需要向上轉,就使用繼承;但如果不需要上溯造型,就應提醒自己防止繼承的濫用。
總結
這篇文章的目的是協助讀者瞭解、掌握抽象類別和介面的特點和不同的使用情境,後面寫著寫著又多嘮叨了幾句,希望對你有協助。
這個系列的目的是協助大家系統、完整的打好基礎、逐漸深入學習,如果你對這些已經很熟了,請不要吝嗇你的評價,多多指出問題,我們一起做的更好!
歡迎關注,第一時間擷取新文章。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。