自從給小白寫了兩篇科普性質的文章後,我就有點一發不可收拾,覺得很有必要繼續寫下去。因為有讀者留言“鼓勵”我說,“二哥,你真的是為小白操碎了心啊!”我容易嗎?我。
當我們要完成的任務是確定的,但具體的方式需要隨後開個會投票的話,Java 的抽象類就派上用場了。這句話怎麼理解呢?搬個小板凳坐好,聽我來給你講講。
01、抽象類的 5 個關鍵點
1)定義抽象類的時候需要用到關鍵字 abstract
,放在 class
關鍵字前。
public abstract class AbstractPlayer {
}
關於抽象類的命名,阿里出品的 Java 開發手冊上有強調,“抽象類命名要使用 Abstract 或 Base 開頭”,記住了哦。
2)抽象類不能被例項化,但可以有子類。
嘗試通過 new
關鍵字例項化的話,編譯器會報錯,提示“類是抽象的,不能例項化”。
通過 extends
關鍵字可以繼承抽象類,繼承後,BasketballPlayer 類就是 AbstractPlayer 的子類。
public class BasketballPlayer extends AbstractPlayer {
}
3)如果一個類定義了一個或多個抽象方法,那麼這個類必須是抽象類。
當在一個普通類(沒有使用 abstract
關鍵字修飾)中定義了抽象方法,編譯器就會有兩處錯誤提示。
第一處在類級別上,提醒你“這個類必須通過 abstract
關鍵字定義”,or 的那個資訊沒必要,見下圖。
第二處在方法級別上,提醒你“抽象方法所在的類不是抽象的”,見下圖。
4)抽象類可以同時宣告抽象方法和具體方法,也可以什麼方法都沒有,但沒必要。就像下面這樣:
public abstract class AbstractPlayer {
abstract void play();
public void sleep() {
System.out.println("運動員也要休息而不是挑戰極限");
}
}
5)抽象類派生的子類必須實現父類中定義的抽象方法。比如說,抽象類中定義了 play()
方法,子類中就必須實現。
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是張伯倫,籃球場上得過 100 分");
}
}
如果沒有實現的話,編譯器會提醒你“子類必須實現抽象方法”,見下圖。
02、什麼時候用抽象類
與抽象類息息相關的還有一個概念,就是介面,我們留到下一篇文章中詳細說,因為要說的知識點還是蠻多的。你現在只需要有這樣一個概念就好,介面是對行為的抽象,抽象類是對整個類(包含成員變數和行為)進行抽象。
(是不是有點明白又有點不明白,彆著急,翹首以盼地等下一篇文章出爐吧)
除了介面之外,還有一個概念就是具體的類,就是不通過 abstract
修飾的普通類,見下面這段程式碼中的定義。
public class BasketballPlayer {
public void play() {
System.out.println("我是詹姆斯,現役第一人");
}
}
有介面,有具體類,那什麼時候該使用抽象類呢?
1)我們希望一些通用的功能被多個子類複用。比如說,AbstractPlayer 抽象類中有一個普通的方法 sleep()
,表明所有運動員都需要休息,那麼這個方法就可以被子類複用。
public abstract class AbstractPlayer {
public void sleep() {
System.out.println("運動員也要休息而不是挑戰極限");
}
}
雖然 AbstractPlayer 類可以不是抽象類——把 abstract
修飾符去掉也能滿足這種場景。但 AbstractPlayer 類可能還會有一個或者多個抽象方法。
BasketballPlayer 繼承了 AbstractPlayer 類,也就擁有了 sleep()
方法。
public class BasketballPlayer extends AbstractPlayer {
}
BasketballPlayer 物件可以直接呼叫 sleep()
方法:
BasketballPlayer basketballPlayer = new BasketballPlayer();
basketballPlayer.sleep();
FootballPlayer 繼承了 AbstractPlayer 類,也就擁有了 sleep()
方法。
public class FootballPlayer extends AbstractPlayer {
}
FootballPlayer 物件也可以直接呼叫 sleep()
方法:
FootballPlayer footballPlayer = new FootballPlayer();
footballPlayer.sleep();
2)我們需要在抽象類中定義好 API,然後在子類中擴充套件實現。比如說,AbstractPlayer 抽象類中有一個抽象方法 play()
,定義所有運動員都可以從事某項運動,但需要對應子類去擴充套件實現。
public abstract class AbstractPlayer {
abstract void play();
}
BasketballPlayer 繼承了 AbstractPlayer 類,擴充套件實現了自己的 play()
方法。
public class BasketballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是張伯倫,我籃球場上得過 100 分,");
}
}
FootballPlayer 繼承了 AbstractPlayer 類,擴充套件實現了自己的 play()
方法。
public class FootballPlayer extends AbstractPlayer {
@Override
void play() {
System.out.println("我是C羅,我能接住任意高度的頭球");
}
}
3)如果父類與子類之間的關係符合 is-a
的層次關係,就可以使用抽象類,比如說籃球運動員是運動員,足球運動員是運動員。
03、具體示例
為了進一步展示抽象類的特性,我們再來看一個具體的示例。假設現在有一個檔案,裡面的內容非常簡單——“Hello World”,現在需要有一個讀取器將內容讀取出來,最好能按照大寫的方式,或者小寫的方式。
這時候,最好定義一個抽象類,比如說 BaseFileReader:
public 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 修飾,表明該成員變數可以在需要時被子類訪問。
readFile()
方法用來讀取檔案,方法體裡面呼叫了抽象方法 mapFileLine()
——需要子類擴充套件實現大小寫的方式。
你看,BaseFileReader 設計的就非常合理,並且易於擴充套件,子類只需要專注於具體的大小寫實現方式就可以了。
小寫的方式:
public class LowercaseFileReader extends BaseFileReader {
protected LowercaseFileReader(Path filePath) {
super(filePath);
}
@Override
protected String mapFileLine(String line) {
return line.toLowerCase();
}
}
大寫的方式:
public class UppercaseFileReader extends BaseFileReader {
protected UppercaseFileReader(Path filePath) {
super(filePath);
}
@Override
protected String mapFileLine(String line) {
return line.toUpperCase();
}
}
你看,從檔案裡面一行一行讀取內容的程式碼被子類複用了——抽象類 BaseFileReader 類中定義的普通方法 readFile()
。與此同時,子類只需要專注於自己該做的工作,LowercaseFileReader 以小寫的方式讀取檔案內容,UppercaseFileReader 以大寫的方式讀取檔案內容。
接下來,我們來新建一個測試類 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());
}
}
專案的 resource 目錄下有一個文字檔案,名字叫 helloworld.txt。
可以通過 ClassLoader.getResource()
的方式獲取到該檔案的 URI 路徑,然後就可以使用 LowercaseFileReader 和 UppercaseFileReader 兩種方式讀取到文字內容了。
輸出結果如下所示:
[hello world]
[HELLO WORLD]
好了,我親愛的讀者朋友,以上就是本文的全部內容了。是不是感覺認知邊界又拓寬了?
我是沉默王二,一枚有趣的程式設計師。如果覺得文章對你有點幫助,請微信搜尋「 沉默王二 」第一時間閱讀,回覆【666】更有我為你精心準備的 500G 高清教學視訊(已分門別類)。
本文 GitHub 已經收錄,有大廠面試完整考點,歡迎 Star。
原創不易,莫要白票,請你為本文點個贊吧,這將是我寫作更多優質文章的最強動力。