面試常客:談談你對抽象類和接口的理解

抽象方法 即使用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);//沒有方法體
}

特點與區別:

抽象類的特點
  1. 抽象類的初衷是“抽象”,即規定這個類“是什麼”,具體的實現暫不確定,是不完整的,因此不允許直接創建實例
  2. 抽象類是由子類具有相同的一類特徵抽象而來,也可以說是其基類或者父類
  3. 抽象方法必須爲 public 或者 protected(因爲如果爲 private,則不能被子類繼承,子類便無法實現該方法),缺省情況下默認爲 public
  4. 抽象類不能用來創建對象
  5. 抽象方法必須由子類來實現
  6. 如果一個類繼承於一個抽象類,則子類必須實現父類的抽象方法,如果子類沒有實現父類的抽象方法,則必須將子類也定義爲抽象類
  7. 抽象類還是很有用的重構工具,因爲它們使得我們可以很容易地將公共方法沿着繼承層次結構向上移動

接口的特點:

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 中需要添加其他功能,可以再創建對應的接口,然後予以實現。

小結

通過上面的代碼例子,我們可以很清晰地瞭解下面這張圖總結的內容。

这里写图片描述

圖片來自:www.jianshu.com/p/8f0a7e22b…

我們可以瞭解到抽象類和接口的這些不同:

  • 抽象層次不同
    • 抽象類是對類抽象,而接口是對行爲的抽象
    • 抽象類是對整個類整體進行抽象,包括屬性、行爲,但是接口卻是對類局部行爲進行抽象
  • 跨域不同
    • 抽象類所跨域的是具有相似特點的類,而接口卻可以跨域不同的類
    • 抽象類所體現的是一種繼承關係,考慮的是子類與父類本質**“是不是”**同一類的關係
    • 而接口並不要求實現的類與接口是同一本質,它們之間只存在**“有沒有這個能力”**的關係
  • 設計層次不同
    • 抽象類是自下而上的設計,在子類中重複出現的工作,抽象到抽象類
    • 接口是自上而下,定義行爲和規範

如何選擇

現在我們知道了,抽象類定義了“是什麼”,可以有非抽象的屬性和方法;接口是更純的抽象類,在 Java 中可以實現多個接口,因此接口表示“具有什麼能力”。

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

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

抽象與多態

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

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

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

面向接口編程

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

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

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

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

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

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

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

轉自:https://juejin.im/entry/59fa7b07518825076a0c4a0c

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章