Java 基礎鞏固:再談抽象類和介面

Love Lenka發表於2017-11-02

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

主要貢獻者:

讀完本文你將瞭解:

背景介紹

這裡寫圖片描述

大家好,這篇文章是 《安卓進階技能樹計劃》 的第一部分 《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 protecteddefault,但不能是 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 中可以實現多個介面,因此介面表示“具有什麼能力”。

在進行選擇時,可以參考以下幾點:

  • 若使用介面,我們可以同時獲得抽象類以及介面的好處
  • 所以假如想建立的基類沒有任何方法定義或者成員變數,那麼無論如何都願意使用介面,而不要選擇抽象類
  • 如果事先知道某種東西會成為基礎類,那麼第一個選擇就是把它變成一個介面
  • 只有在必須使用方法定義或者成員變數的時候,才應考慮採用抽象類

此外使用介面最重要的一個原因:實現介面可以使一個類向上轉型至多個基礎類。

比如 SerializableCloneable 這樣常見的介面,一個類實現後就表示有這些能力,它可以被當做 SerializableCloneable 進行處理。

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

抽象與多型

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

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

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

面向介面程式設計

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

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

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

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

  1. 通過建構函式傳遞依賴物件
    • 比如在建構函式中的需要傳遞的引數是抽象類或介面的方式實現
  2. 通過 setter 方法傳遞依賴物件
    • 即在我們設定的 setXXX 方法中的引數為抽象類或介面,來實現傳遞依賴物件
  3. 介面宣告實現依賴物件,也叫介面注入
    • 即在函式宣告中引數為抽象類或介面,來實現傳遞依賴物件,從而達到直接使用依賴物件的目的。

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

多型

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

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

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

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

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

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

總結

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

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

文章同步傳送於微信公眾號:安卓進化論,歡迎關注,第一時間獲取新文章。

Thanks

相關文章