小白,你要的Java抽象類,操碎了心!

自從給小白寫了兩篇科普性質的文章後,我就有點一發不可收拾,覺得很有必要繼續寫下去。因為有讀者留言「鼓勵」我說,「二哥,你真的是為小白操碎了心啊!」我容易嗎?我。

當我們要完成的任務是確定的,但具體的方式需要隨後開個會投票的話,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。

原創不易,莫要白票,請你為本文點個贊吧,這將是我寫作更多優質文章的最強動力。