抽象類就是抽象的類,抽象是相對于具體而言的,一般而言,具體類有直接對應的對象,而抽象類沒有,它表達的是抽象概念 ... 但為什麼非要顯式将類設為抽象的呢?抽象類和接口某些地方很像,但其實根本不同,不過又互相聯系,它們到底有着怎樣的關系呢?
本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》,由機械工業出版社華章分社出版,于2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營連結:http://item.jd.com/12299018.html

基本概念
上節提到了一個概念,抽象類,抽象類是什麼呢?顧名思義,抽象類就是抽象的類,抽象是相對于具體而言的,一般而言,具體類有直接對應的對象,而抽象類沒有,它表達的是抽象概念,一般是具體類的比較上層的父類。
比如說,狗是具體對象,而動物則是抽象概念,櫻桃是具體對象,而水果則是抽象概念,正方形是具體對象,而圖形則是抽象概念。下面我們通過一些例子來說明Java中的抽象類。
抽象方法和抽象類
之前我們介紹過圖形類Shape,它有一個方法draw(),Shape其實是一個抽象概念,它的draw方法其實并不知道如何實作,隻有子類才知道。這種隻有子類才知道如何實作的方法,一般被定義為抽象方法。
抽象方法是相對于具體方法而言的,具體方法有實作代碼,而抽象方法隻有聲明,沒有實作,上節介紹的接口中的方法就都是抽象方法。
抽象方法和抽象類都使用abstract這個關鍵字來聲明,文法如下所示:
public abstract class Shape {
// ... 其他代碼
public abstract void draw();
}
定義了抽象方法的類必須被聲明為抽象類,不過,抽象類可以沒有抽象方法。抽象類和具體類一樣,可以定義具體方法、執行個體變量等,它和具體類的核心差別是,抽象類不能建立對象(比如,不能使用new Shape()),而具體類可以。
抽象類不能建立對象,要建立對象,必須使用它的具體子類。一個類在繼承抽象類後,必須實作抽象類中定義的所有抽象方法,除非它自己也聲明為抽象類。圓類的實作代碼,如下所示:
public class Circle extends Shape {
//...其他代碼
@Override
public void draw() {
// ....
}
}
圓實作了draw()方法。與接口類似,抽象類雖然不能使用new,但可以聲明抽象類的變量,引用抽象類具體子類的對象,如下所示:
Shape shape = new Circle();
shape.draw();
shape是抽象類Shape類型的變量,引用了具體子類Circle的對象,調用draw方法将調用Circle的draw代碼。
為什麼需要抽象類?
抽象方法和抽象類看上去是多餘的,對于抽象方法,不知道如何實作,定義一個空方法體不就行了嗎,而抽象類不讓建立對象,看上去隻是增加了一個不必要的限制。
引入抽象方法和抽象類,是Java提供的一種文法工具,對于一些類和方法,引導使用者正确使用它們,減少被誤用。
使用抽象方法,而非空方法體,子類就知道他必須要實作該方法,而不可能忽略。
使用抽象類,類的使用者建立對象的時候,就知道他必須要使用某個具體子類,而不可能誤用不完整的父類。
無論是寫程式,還是平時做任何别的事情的時候,每個人都可能會犯錯,減少錯誤不能隻依賴人的優秀素質,還需要一些機制,使得一個普通人都容易把事情做對,而難以把事情做錯。抽象類就是Java提供的這樣一種機制。
抽象類和接口
抽象類和接口有類似之處,都不能用于建立對象,接口中的方法其實都是抽象方法。如果抽象類中隻定義了抽象方法,那抽象類和接口就更像了。但抽象類和接口根本上是不同的,一個類可以實作多個接口,但隻能繼承一個類。
抽象類和接口是配合而非替代關系,它們經常一起使用,接口聲明能力,抽象類提供預設實作,實作全部或部分方法,一個接口經常有一個對應的抽象類。
比如說,在Java類庫中,有:
- Collection接口和對應的AbstractCollection抽象類
- List接口和對應的AbstractList抽象類
- Map接口和對應的AbstractMap抽象類
對于需要實作接口的具體類而言,有兩個選擇,一個是實作接口,自己實作全部方法,另一個則是繼承抽象類,然後根據需要重寫方法。
繼承的好處是複用代碼,隻重寫需要的即可,需要寫的代碼比較少,容易實作。不過,如果這個具體類已經有父類了,那就隻能選擇實作接口了。
我們以一個例子來進一步說明這種配合關系,還是用前面兩節中關于add的例子,上節引入了IAdd接口,代碼如下:
public interface IAdd {
void add(int number);
void addAll(int[] numbers);
}
我們實作一個抽象類AbstractAdder,代碼如下:
public abstract class AbstractAdder implements IAdd {
@Override
public void addAll(int[] numbers) {
for(int num : numbers){
add(num);
}
}
}
這個抽象類提供了addAll方法的實作,它通過調用add方法來實作,而add方法是一個抽象方法。
這樣,對于需要實作IAdd接口的類來說,它可以選擇直接實作IAdd接口,或者從AbstractAdder類繼承,如果繼承,隻需要實作add方法就可以了。這裡,我們讓原有的Base類繼承AbstractAdder,代碼如下所示:
public class Base extends AbstractAdder {
private static final int MAX_NUM = 1000;
private int[] arr = new int[MAX_NUM];
private int count;
@Override
public void add(int number){
if(count<MAX_NUM){
arr[count++] = number;
}
}
}
小結
本節,我們談了抽象類,相對于具體類,它用于表達抽象概念,雖然從文法上,抽象類不是必須的,但它能使程式更為清晰,減少誤用,抽象類和接口經常互相配合,接口定義能力,而抽象類提供預設實作,友善子類實作接口。
在目前關于類的描述中,每個類都是獨立的,都對應一個Java源代碼檔案,但在Java中,一個類還可以放在另一個類的内部,稱之為内部類,為什麼要将一個類放到别的類内部呢?
----------------
未完待續,檢視最新文章,敬請關注微信公衆号“老馬說程式設計”(掃描下方二維碼),從入門到進階,深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心寫作,原創文章,保留所有版權。
-----------
更多相關原創文章
計算機程式的思維邏輯 (13) - 類
計算機程式的思維邏輯 (14) - 類的組合
計算機程式的思維邏輯 (15) - 初識繼承和多态
計算機程式的思維邏輯 (16) - 繼承的細節
計算機程式的思維邏輯 (17) - 繼承實作的基本原理
計算機程式的思維邏輯 (18) - 為什麼說繼承是把雙刃劍
計算機程式的思維邏輯 (19) - 接口的本質
計算機程式的思維邏輯 (21) - 内部類的本質