Java 基礎夯實1:細談抽象類別和介面,夯實細談

來源:互聯網
上載者:User

Java 基礎夯實1:細談抽象類別和介面,夯實細談

讀完本文你將瞭解:

  • 背景介紹

  • 什麼是抽象類別和介面

  • 特點與區別

    • 抽象類別的特點

    • 介面的特點

    • 栗子

    • 小結

  • 如何選擇

  • 抽象與多態

    • 面向介面編程

    • 多態

    • 繼承和組合

  • 總結

文章出自:安卓進階學習指南

主要貢獻者:

  • Milo

  • Struggle

  • shixinzhang

背景介紹

大家好,這篇文章是 《安卓進階技能樹計劃》 的第一部分 《Java 基礎系列》 的第一篇。

距離上一篇預告 《Java 基礎夯實系列上線預告》 過去了很久,之所以這麼慢,是因為我們做這個活動,除了要保證知識點的全面、完整,還想要讓每一篇文章都有自己的思考,儘可能的將知識點與實踐結合,努力讓讀者讀了有所收穫。每位小夥伴都有工作在身,每個知識點都需要經過思考、學習、寫作、提交、審核、修改、編輯、發布等多個過程,所以整體下來時間就會慢一些,這裡先向各位道歉。

《Java 基礎系列》初步整理大概有 12 篇,主要內容為。:

  1. 抽象類別和介面

  2. 內部類

  3. 修飾符

  4. 裝箱拆箱

  5. 註解

  6. 反射

  7. 泛型

  8. 異常

  9. 集合

  10. IO

  11. 字串

  12. 其他

第一篇我們來聊聊抽象類別和介面

“抽象類別和介面”聽起來是非常普遍的東西,有些朋友會覺得:這個太基礎了吧,有啥好說的,你又來糊弄我。

事實上我在面試中不僅一次被問到相關的問題:

  • 抽象類別和介面之間的區別?

  • 什麼時候建立抽象類別?什麼時候建立介面?

  • 設計架構時該如何選擇?

我比較喜歡這樣的問題,答案可深可淺,體現了我們對日常工作的思考。

我們什麼時候會建立一個抽象類別?什麼時候會建立一個介面呢?當轉換一下思維,不僅僅為了完成功能,而是要保證整個項目架構的穩定靈活可擴充性,你會如何選擇呢?

這篇文章我們努力回答這些問題,也希望你可以說出你的答案。

什麼是抽象類別和介面

抽象方法 即使用 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 修飾的**靜態常量**

    • 可以通過類命名直接存取:ImplementClass.name

    • 不推薦使用介面建立常量類

  • 實現介面的非抽象類別必須實現介面中所有方法,抽象類別可以不用全部實現

  • 介面不能建立對象,但可以申明一個介面變數,方便調用

  • 完全解耦,可以編寫可複用性更好的代碼

栗子

前面說了太多,我們直接上代碼。

假設我們新開始一個項目,需要寫大量的 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 進行處理。

推薦介面和抽象類別同時使用,這樣既保證了資料的安全性又可以實現多繼承。

抽象與多態

俗話說:“做事留一線,日後好相見”。

程式開發也一樣,它是一個不斷遞增或者累積的過程,不可能一次做到完美,所以我們要儘可能地給後面修改留有餘地,而這就需要我們使用傳說中“物件導向的三個特徵” --- 繼承、封裝、多態。

不管使用抽象類別還是介面,歸根接地還是儘可能地職責分離,把業務抽象,也就是“面向介面編程”。

面向介面編程

日常生活裡與人約定時,一般不要說得太具體。就好比別人問我們什麼時候有空,回一句“大約在冬季” 一定比 “這周六中午” 靈活一點,誰知道這周六會不會突然有什麼變故。

我們在寫代碼時追求的是“以不變應萬變”,在需求變更時,儘可能少地修改代碼就可以實現。

而這,就需要模組之間依賴時,最好都只依賴對方給的抽象介面,而不是具體實現。

在設計模式裡這就是“依賴倒置原則”,依賴倒置有三種方式來實現:

  1. 通過建構函式傳遞依賴對象

  • 比如在建構函式中的需要傳遞的參數是抽象類別或介面的方式實現

通過 setter 方法傳遞依賴對象

  • 即在我們設定的 setXXX 方法中的參數為抽象類別或介面,來實現傳遞依賴對象

介面聲明實現依賴對象,也叫介面注入

  • 即在函式宣告中參數為抽象類別或介面,來實現傳遞依賴對象,從而達到直接使用依賴對象的目的。

可以看到,“面向介面編程”說的“介面”也包括抽象類別,其實說的是基類,越簡單越好。

多態

多態指的是編譯期只知道是個人,具體是什麼樣的人需要在運行時能確定,同樣的參數有可能會有不同的實現。

通過抽象建立規範,在運行時替換成具體的對象,保證系統的擴充性、靈活性。

實現多態主要有以下三種方式:

  1. 介面實現

  2. 繼承父類重寫方法

  3. 同一類中進行方法重載

不論哪種實現方式,調用者持有的都是基類,不同的實現在他看來都是基類,使用時也當基類用。

這就是“向上轉型”,即:子類在被調用過程中由繼承關係的下方轉變成上面的角色。

向上轉型是能力減少的過程,編譯器可以幫我們實現;但 “向下轉型”是能力變強的過程,需要進行強轉。

以上面的代碼為例:

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" 關係。

為判斷自己到底應該選用合成還是繼承,一個最簡單的辦法就是考慮是否需要從新類向上轉型回基礎類。

假如的確需要向上轉,就使用繼承;但如果不需要上溯造型,就應提醒自己防止繼承的濫用。

總結

這篇文章的目的是協助讀者瞭解、掌握抽象類別和介面的特點和不同的使用情境,後面寫著寫著又多嘮叨了幾句,希望對你有協助。

這個系列的目的是協助大家系統、完整的打好基礎、逐漸深入學習,如果你對這些已經很熟了,請不要吝嗇你的評價,多多指出問題,我們一起做的更好!

歡迎關注,第一時間擷取新文章。



著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.