《Java 基礎系列》初步整理
《Java 基礎系列》初步整理大概有 12 篇,主要內容為。:
抽象類和接口
內部類
修飾符
裝箱拆箱
註解
反射
泛型
異常
集合
IO
字符串
其他
第一篇我們來聊聊抽象類和接口。
“抽象類和接口”聽起來是非常普遍的東西,有些朋友會覺得:這個太基礎了吧,有啥好說的,你又來糊弄我。
這裏寫圖片描述
事實上我在面試中不僅一次被問到相關的問題:
抽象類和接口之間的區別?
什麽時候創建抽象類?什麽時候創建接口?
設計框架時該如何選擇?
我比較喜歡這樣的問題,答案可深可淺,體現了我們對日常工作的思考。
我們什麽時候會創建一個抽象類?什麽時候會創建一個接口呢?當轉換一下思維,不僅僅為了完成功能,而是要保證整個項目架構的穩定靈活可擴展性,你會如何選擇呢?
這篇文章我們努力回答這些問題,也希望你可以說出你的答案。
什麽是抽象類和接口
抽象方法 即使用 abstract 關鍵字修飾,僅有聲明沒有方法體的方法。
public abstract void f(); //沒有內容
1
抽象類 即包含抽象方法的類。
如果一個類包含一個或者多個抽象方法,該類必須被限定為抽象的。抽象類可以不包含抽象方法。
public abstract class BaseActivity {
private final String TAG = this.getClass().getSimpleName(); //抽象類可以有成員
void log(String msg){ //抽象類可以有具體方法
System.out.println(msg);
}是抽象類的一種特殊形式,使用 interface 修飾。
public interface OnClickListener {
void onClick(View v);
}
1
2
3
特點與區別
抽象類的特點
抽象類的初衷是“抽象”,即規定這個類“是什麽”,具體的實現暫不確定,是不完整的,因此不允許直接創建實例。
抽象類是由子類具有相同的一類特征抽象而來,也可以說是其基類或者父類
抽象方法必須為 public 或者 protected(因為如果為 private,則不能被子類繼承,子類便無法實現該方法),缺省情況下默認為 public
抽象類不能用來創建對象
抽象方法必須由子類來實現
如果一個類繼承於一個抽象類,則子類必須實現父類的抽象方法,如果子類沒有實現父類的抽象方法,則必須將子類也定義為抽象類
抽象類還是很有用的重構工具,因為它們使得我們可以很容易地將公共方法沿著繼承層次結構向上移動
接口的特點
Java 為了保證數據安全性是不能多繼承的,也就是一個類只有一個父類。
但是接口不同,一個類可以同時實現多個接口,不管這些接口之間有沒有關系,所以接口彌補了抽象類不能多繼承的缺陷。
接口是抽象類的延伸,它可以定義沒有方法體的方法,要求實現者去實現。
接口的所有方法訪問權限自動被聲明為 public
接口中可以定義“成員變量”,會自動變為 public static final 修飾的靜態常量
可以通過類命名直接訪問:www.yibaoyule1.com 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(www.qinlinyu.cn 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);
}
1
2
3
接口默認是 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) caihonyule.com/ {
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/www.huachengjpt.com p/8f0a7e22bb8c
我們可以了解到抽象類和接口的這些不同:
抽象層次不同
抽象類是對類抽象,而接口是對行為的抽象
抽象類是對整個類整體進行抽象,包括屬性、行為,但是接口卻是對類局部行為進行抽象
跨域不同
抽象類所跨域的是具有相似特點的類,而接口卻可以跨域不同的類
抽象類所體現的是一種繼承關系,考慮的是子類與父類本質“是不是”同一類的關系
而接口並不要求實現的類與接口是同一本質,它們之間只存在“有沒有這個能力”的關系
設計層次不同
抽象類是自下而上的設計,在子類中重復出現的工作,抽象到抽象類中
接口是自上而下,定義行為和規範
如何選擇
現在我們知道了,抽象類定義了“是什麽”,可以有非抽象的屬性和方法;接口是更純的抽象類,在 Java 中可以實現多個接口,因此接口表示“具有什麽能力”。
在進行選擇時,可以參考以下幾點:
若使用接口,我們可以同時獲得抽象類以及接口的好處
所以假如想創建的基類沒有任何方法定義或者成員變量,那麽無論如何都願意使用接口,而不要選擇抽象類
如果事先知道某種東西會成為基礎類,那麽第一個選擇就是把它變成一個接口
只有在必須使用方法定義或者成員變量的時候,才應考慮采用抽象類
此外使用接口最重要的一個原因:實現接口可以使一個類向上轉型至多個基礎類。
比如 Serializable 和 Cloneable 這樣常見的接口,一個類實現後就表示有這些能力,它可以被當做 Serializable 和 Cloneable 進行處理。
推薦接口和抽象類同時使用,這樣既保證了數據的安全性又可以實現多繼承。
抽象與多態
俗話說:“做事留一線,日後好相見”。
程序開發也一樣,它是一個不斷遞增或者累積的過程,不可能一次做到完美,所以我們要盡可能地給後面修改留有余地,而這就需要我們使用傳說中“面向對象的三個特征” — 繼承、封裝、多態。
不管使用抽象類還是接口,歸根接地還是盡可能地職責分離,把業務抽象,也就是“面向接口編程”。
面向接口編程
日常生活裏與人約定時,一般不要說得太具體。就好比別人問我們什麽時候有空,回一句“大約在冬季” 一定比 “這周六中午” 靈活一點,誰知道這周六會不會突然有什麽變故。
我們在寫代碼時追求的是“以不變應萬變”,在需求變更時,盡可能少地修改代碼就可以實現。
而這,就需要模塊之間依賴時,最好都只依賴對方給的抽象接口,而不是具體實現。
在設計模式裏這就是“依賴倒置原則”,依賴倒置有三種方式來實現:
通過構造函數傳遞依賴對象
比如在構造函數中的需要傳遞的參數是抽象類或接口的方式實現
通過 setter 方法傳遞依賴對象
即在我們設置的 setXXX 方法中的參數為抽象類或接口,來實現傳遞依賴對象
接口聲明實現依賴對象,也叫接口註入
即在函數聲明中參數為抽象類或接口,來實現傳遞依賴對象,從而達到直接使用依賴對象的目的。
可以看到,“面向接口編程”說的“接口”也包括抽象類,其實說的是基類,越簡單越好。
多態
多態指的是編譯期只知道是個人,具體是什麽樣的人需要在運行時能確定,同樣的參數有可能會有不同的實現。
通過抽象建立規範,在運行時替換成具體的對象,保證系統的擴展性、靈活性。
實現多態主要有以下三種方式:
接口實現
繼承父類重寫方法
同一類中進行方法重載
不論哪種實現方式,調用者持有的都是基類,不同的實現在他看來都是基類,使用時也當基類用。
這就是“向上轉型”,即:子類在被調用過程中由繼承關系的下方轉變成上面的角色。
向上轉型是能力減少的過程,編譯器可以幫我們實現;但 “向下轉型”是能力變強的過程,需要進行強轉。
以上面的代碼為例:
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) www.xyseo.net/ {
for (OnLocationChangeListener listener : mListeners) {
LocationObserver 持有的是 OnLocationChangeListener 的引用,不管運行時傳入的是 MainActivity 還是其他 Activity,只要實現了這個接口,就可以被調用實現的方法。
在編譯期就知道要調用的是哪個方法,稱為“前期綁定”(又稱“靜態綁定”),由編譯器和連接程序實現。
在運行期調用正確的方法,這個過程稱為“動態綁定”,要實現動態綁定,就要有一種機制在運行期時可以根據對象的類型調用恰當的方法。這種機制是由虛擬機實現的, invokevirtual 指令會把常量池中的類方法符號引用解析到不同的引用上,這個過程叫做“動態分派”,具體的實現過程我們暫不討論。
繼承和組合
盡管繼承在學習 OOP 的過程中得到了大量的強調,但並不意味著應該盡可能地到處使用它。
相反,使用它時要特別慎重,因為繼承一個類,意味著你需要接受他的一切,不管貧窮富貴生老病死,你都得接受他,你能做到嗎?
一般人都無法做到白頭偕老,所以只有在清楚知道需要繼承所有方法的前提下,才可考慮它。
有一種取代繼承的方式是 “組合”。
組合就是通過持有一個類的引用來擁有他的一切,而不是繼承,在需要調用他的方法時傳入引用,然後調用,否則就清除引用。
組合比繼承靈活在於關系更松一些,繼承表示的是“is-a” 關系,比較強;而組合則是 “has-a” 關系。
為判斷自己到底應該選用合成還是繼承,一個最簡單的辦法就是考慮是否需要從新類向上轉型回基礎類。
假如的確需要向上轉,就使用繼承;但如果不需要上溯造型,就應提醒自己防止繼承的濫用。
總結
這篇文章的目的是幫助讀者了解、掌握抽象類和接口的特點和不同的使用場景,後面寫著寫著又多嘮叨了幾句,希望對你有幫助。
這個系列的目的是幫助大家系統、完整的打好基礎、逐漸深入學習,如果你對這些已經很熟了,請不要吝嗇你的評價,多多指出問題,我們一起做的更好!
《Java 基礎系列》初步整理