“哈哈,是呀,这次不能再断更了,我要再更 175 篇,总计 200 篇,给广大的学弟学妹们一个完整的 Java 学习体系。”我对未来充满了信心。

    “那就开始吧。”三妹说。


    定义抽象类的时候需要用到关键字 ,放在 class 关键字前,就像下面这样。

    关于抽象类的命名,《阿里的 Java 开发手册》上有强调,“抽象类命名要使用 Abstract 或 Base 开头”,这条规约还是值得遵守的。

    抽象类是不能实例化的,尝试通过 new 关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。

    虽然抽象类不能实例化,但可以有子类。子类通过 extends 关键字来继承抽象类。就像下面这样。

    1. public class BasketballPlayer extends AbstractPlayer {
    2. }

    如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。

    当我们尝试在一个普通类中定义抽象方法的时候,编译器会有两处错误提示。第一处在类级别上,提示“这个类必须通过 abstract 关键字定义”,见下图。

    抽象类:子类复用的基石 - 图2

    第二处在尝试定义 abstract 的方法上,提示“抽象方法所在的类不是抽象的”,见下图。

    抽象类中既可以定义抽象方法,也可以定义普通方法,就像下面这样:

    1. public abstract class AbstractPlayer {
    2. abstract void play();
    3. public void sleep() {
    4. System.out.println("运动员也要休息而不是挑战极限");
    5. }
    6. }

    抽象类派生的子类必须实现父类中定义的抽象方法。比如说,抽象类 AbstractPlayer 中定义了 play() 方法,子类 BasketballPlayer 中就必须实现。

    1. public class BasketballPlayer extends AbstractPlayer {
    2. @Override
    3. void play() {
    4. System.out.println("我是张伯伦,篮球场上得过 100 分");
    5. }
    6. }

    抽象类:子类复用的基石 - 图4

    “二哥,抽象方法我明白了,那什么时候使用抽象方法呢?能给我讲讲它的应用场景吗?”三妹及时的插话道。

    “这问题问的恰到好处呀!”我扶了扶眼镜继续说。

    第一种场景

    当我们希望一些通用的功能被多个子类复用的时候,就可以使用抽象类。比如说,AbstractPlayer 抽象类中有一个普通的方法 sleep(),表明所有运动员都需要休息,那么这个方法就可以被子类复用。

    1. abstract class AbstractPlayer {
    2. public void sleep() {
    3. System.out.println("运动员也要休息而不是挑战极限");
    4. }
    5. }

    子类 BasketballPlayer 继承了 AbstractPlayer 类:

    也就拥有了 sleep() 方法。BasketballPlayer 的对象可以直接调用父类的 sleep() 方法:

    1. basketballPlayer.sleep();

    子类 FootballPlayer 继承了 AbstractPlayer 类:

    1. class FootballPlayer extends AbstractPlayer {

    也拥有了 sleep() 方法,FootballPlayer 的对象也可以直接调用父类的 sleep() 方法:

    1. FootballPlayer footballPlayer = new FootballPlayer();
    2. footballPlayer.sleep();

    这样是不是就实现了代码的复用呢?

    第二种场景

    当我们需要在抽象类中定义好 API,然后在子类中扩展实现的时候就可以使用抽象类。比如说,AbstractPlayer 抽象类中定义了一个抽象方法 play(),表明所有运动员都可以从事某项运动,但需要对应子类去扩展实现,表明篮球运动员打篮球,足球运动员踢足球。

    1. abstract class AbstractPlayer {
    2. abstract void play();
    3. }

    BasketballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play() 方法。

    FootballPlayer 继承了 AbstractPlayer 类,扩展实现了自己的 play() 方法。

    1. public class FootballPlayer extends AbstractPlayer {
    2. @Override
    3. void play() {
    4. System.out.println("我是C罗,我能接住任意高度的头球");
    5. }
    6. }

    这时候,最好定义一个抽象类 BaseFileReader:

    1. abstract class BaseFileReader {
    2. protected Path filePath;
    3. protected BaseFileReader(Path filePath) {
    4. this.filePath = filePath;
    5. }
    6. public List<String> readFile() throws IOException {
    7. return Files.lines(filePath)
    8. }
    9. }
    • filePath 为文件路径,使用 protected 修饰,表明该成员变量可以在需要时被子类访问到。

    • readFile() 方法用来读取文件,方法体里面调用了抽象方法 mapFileLine()——需要子类来扩展实现大小写的不同读取方式。

    在我看来,BaseFileReader 类设计的就非常合理,并且易于扩展,子类只需要专注于具体的大小写实现方式就可以了。

    小写的方式:

    1. class LowercaseFileReader extends BaseFileReader {
    2. protected LowercaseFileReader(Path filePath) {
    3. super(filePath);
    4. }
    5. @Override
    6. protected String mapFileLine(String line) {
    7. return line.toLowerCase();
    8. }
    9. }

    大写的方式:

    1. class UppercaseFileReader extends BaseFileReader {
    2. protected UppercaseFileReader(Path filePath) {
    3. super(filePath);
    4. }
    5. @Override
    6. protected String mapFileLine(String line) {
    7. return line.toUpperCase();
    8. }
    9. }

    从文件里面一行一行读取内容的代码被子类复用了。与此同时,子类只需要专注于自己该做的工作,LowercaseFileReader 以小写的方式读取文件内容,UppercaseFileReader 以大写的方式读取文件内容。

    来看一下测试类 FileReaderTest:

    在项目的 resource 目录下建一个文本文件,名字叫 helloworld.txt,里面的内容就是“Hello World”。文件的具体位置如下图所示,我用的集成开发环境是 Intellij IDEA。

    在 resource 目录下的文件可以通过 ClassLoader.getResource() 的方式获取到 URI 路径,然后就可以取到文本内容了。

    输出结果如下所示:


    “完了吗?二哥”三妹似乎还沉浸在聆听教诲的快乐中。

    “是滴,这次我们系统化的学习了抽象类,可以说面面俱到了。三妹你可以把代码敲一遍,加强了一些印象,电脑交给你了。”说完,我就跑到阳台去抽烟了。