教妹學(xué) Java 第 25 講:抽象類
“二哥,你這明顯加快了更新的頻率呀!”三妹對(duì)于我最近的肝勁由衷的佩服了起來。
“哈哈,是呀,這次不能再斷更了,我要再更 175 篇,總計(jì) 200 篇,給廣大的學(xué)弟學(xué)妹們一個(gè)完整的 Java 學(xué)習(xí)體系?!蔽覍?duì)未來充滿了信心。
“那就開始吧?!比谜f。

定義抽象類的時(shí)候需要用到關(guān)鍵字 abstract,放在 class 關(guān)鍵字前,就像下面這樣。
abstract class AbstractPlayer {
}
關(guān)于抽象類的命名,《阿里的 Java 開發(fā)手冊(cè)》上有強(qiáng)調(diào),“抽象類命名要使用 Abstract 或 Base 開頭”,這條規(guī)約還是值得遵守的。
抽象類是不能實(shí)例化的,嘗試通過 new 關(guān)鍵字實(shí)例化的話,編譯器會(huì)報(bào)錯(cuò),提示“類是抽象的,不能實(shí)例化”。

雖然抽象類不能實(shí)例化,但可以有子類。子類通過 extends 關(guān)鍵字來繼承抽象類。就像下面這樣。
public class BasketballPlayer extends AbstractPlayer {
}
如果一個(gè)類定義了一個(gè)或多個(gè)抽象方法,那么這個(gè)類必須是抽象類。
當(dāng)我們嘗試在一個(gè)普通類中定義抽象方法的時(shí)候,編譯器會(huì)有兩處錯(cuò)誤提示。第一處在類級(jí)別上,提示“這個(gè)類必須通過 abstract 關(guān)鍵字定義”,見下圖。

第二處在嘗試定義 abstract 的方法上,提示“抽象方法所在的類不是抽象的”,見下圖。

抽象類中既可以定義抽象方法,也可以定義普通方法,就像下面這樣:
public abstract class AbstractPlayer {
abstract void play();
public void sleep() {
System.out.println("運(yùn)動(dòng)員也要休息而不是挑戰(zhàn)極限");
}
}
抽象類派生的子類必須實(shí)現(xiàn)父類中定義的抽象方法。比如說,抽象類 AbstractPlayer 中定義了 play() 方法,子類 BasketballPlayer 中就必須實(shí)現(xiàn)。
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是張伯倫,籃球場(chǎng)上得過 100 分");
}
}
如果沒有實(shí)現(xiàn)的話,編譯器會(huì)提示“子類必須實(shí)現(xiàn)抽象方法”,見下圖。

“二哥,抽象方法我明白了,那什么時(shí)候使用抽象方法呢?能給我講講它的應(yīng)用場(chǎng)景嗎?”三妹及時(shí)的插話道。
“這問題問的恰到好處呀!”我扶了扶眼鏡繼續(xù)說。
第一種場(chǎng)景。
當(dāng)我們希望一些通用的功能被多個(gè)子類復(fù)用的時(shí)候,就可以使用抽象類。比如說,AbstractPlayer 抽象類中有一個(gè)普通的方法 sleep(),表明所有運(yùn)動(dòng)員都需要休息,那么這個(gè)方法就可以被子類復(fù)用。
abstract class AbstractPlayer {
public void sleep() {
System.out.println("運(yùn)動(dòng)員也要休息而不是挑戰(zhàn)極限");
}
}
子類 BasketballPlayer 繼承了 AbstractPlayer 類:
class BasketballPlayer extends AbstractPlayer {
}
也就擁有了 sleep() 方法。BasketballPlayer 的對(duì)象可以直接調(diào)用父類的 sleep() 方法:
BasketballPlayer basketballPlayer = new BasketballPlayer();
basketballPlayer.sleep();
子類 FootballPlayer 繼承了 AbstractPlayer 類:
class FootballPlayer extends AbstractPlayer {
}
也擁有了 sleep() 方法,F(xiàn)ootballPlayer 的對(duì)象也可以直接調(diào)用父類的 sleep() 方法:
FootballPlayer footballPlayer = new FootballPlayer();
footballPlayer.sleep();
這樣是不是就實(shí)現(xiàn)了代碼的復(fù)用呢?
第二種場(chǎng)景。
當(dāng)我們需要在抽象類中定義好 API,然后在子類中擴(kuò)展實(shí)現(xiàn)的時(shí)候就可以使用抽象類。比如說,AbstractPlayer 抽象類中定義了一個(gè)抽象方法 play(),表明所有運(yùn)動(dòng)員都可以從事某項(xiàng)運(yùn)動(dòng),但需要對(duì)應(yīng)子類去擴(kuò)展實(shí)現(xiàn),表明籃球運(yùn)動(dòng)員打籃球,足球運(yùn)動(dòng)員踢足球。
abstract class AbstractPlayer {
abstract void play();
}
BasketballPlayer 繼承了 AbstractPlayer 類,擴(kuò)展實(shí)現(xiàn)了自己的 play() 方法。
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是張伯倫,我籃球場(chǎng)上得過 100 分,");
}
}
FootballPlayer 繼承了 AbstractPlayer 類,擴(kuò)展實(shí)現(xiàn)了自己的 play() 方法。
public class FootballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是C羅,我能接住任意高度的頭球");
}
}
為了進(jìn)一步展示抽象類的特性,我們?cè)賮砜匆粋€(gè)具體的示例。假設(shè)現(xiàn)在有一個(gè)文件,里面的內(nèi)容非常簡(jiǎn)單,只有一個(gè)“Hello World”,現(xiàn)在需要有一個(gè)讀取器將內(nèi)容從文件中讀取出來,最好能按照大寫的方式,或者小寫的方式來讀。
這時(shí)候,最好定義一個(gè)抽象類 BaseFileReader:
abstract class BaseFileReader {
protected Path filePath;
protected BaseFileReader(Path filePath) {
this.filePath = filePath;
}
public List<String> readFile() throws IOException {
return Files.lines(filePath)
.map(this::mapFileLine).collect(Collectors.toList());
}
protected abstract String mapFileLine(String line);
}
filePath 為文件路徑,使用 protected 修飾,表明該成員變量可以在需要時(shí)被子類訪問到。
readFile()方法用來讀取文件,方法體里面調(diào)用了抽象方法mapFileLine()——需要子類來擴(kuò)展實(shí)現(xiàn)大小寫的不同讀取方式。
在我看來,BaseFileReader 類設(shè)計(jì)的就非常合理,并且易于擴(kuò)展,子類只需要專注于具體的大小寫實(shí)現(xiàn)方式就可以了。
小寫的方式:
class LowercaseFileReader extends BaseFileReader {
protected LowercaseFileReader(Path filePath) {
super(filePath);
}
@Override
protected String mapFileLine(String line) {
return line.toLowerCase();
}
}
大寫的方式:
class UppercaseFileReader extends BaseFileReader {
protected UppercaseFileReader(Path filePath) {
super(filePath);
}
@Override
protected String mapFileLine(String line) {
return line.toUpperCase();
}
}
從文件里面一行一行讀取內(nèi)容的代碼被子類復(fù)用了。與此同時(shí),子類只需要專注于自己該做的工作,LowercaseFileReader 以小寫的方式讀取文件內(nèi)容,UppercaseFileReader 以大寫的方式讀取文件內(nèi)容。
來看一下測(cè)試類 FileReaderTest:
public class FileReaderTest {
public static void main(String[] args) throws URISyntaxException, IOException {
URL location = FileReaderTest.class.getClassLoader().getResource("helloworld.txt");
Path path = Paths.get(location.toURI());
BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);
BaseFileReader uppercaseFileReader = new UppercaseFileReader(path);
System.out.println(lowercaseFileReader.readFile());
System.out.println(uppercaseFileReader.readFile());
}
}
在項(xiàng)目的 resource 目錄下建一個(gè)文本文件,名字叫 helloworld.txt,里面的內(nèi)容就是“Hello World”。文件的具體位置如下圖所示,我用的集成開發(fā)環(huán)境是 Intellij IDEA。

在 resource 目錄下的文件可以通過 ClassLoader.getResource() 的方式獲取到 URI 路徑,然后就可以取到文本內(nèi)容了。
輸出結(jié)果如下所示:
[hello world]
[HELLO WORLD]
“完了嗎?二哥”三妹似乎還沉浸在聆聽教誨的快樂中。
“是滴,這次我們系統(tǒng)化的學(xué)習(xí)了抽象類,可以說面面俱到了。三妹你可以把代碼敲一遍,加強(qiáng)了一些印象,電腦交給你了?!闭f完,我就跑到陽臺(tái)去抽煙了。
“呼。。。。?!币粋€(gè)大大的眼圈飄散開來,又是愉快的一天~
