1. 概述

在实现某种契约时,我们常常希望将部分逻辑的实现推迟到后续阶段。在 Java 中,抽象类(abstract class)就是为此而生的一种机制。

本文将带你快速了解 Java 中抽象类的基础知识,并分析其适用场景

2. 抽象类的核心概念

在深入使用抽象类之前,先明确几个关键点:

  • ✅ 抽象类需要使用 abstract 关键字修饰类声明
  • ❌ 抽象类不能被直接实例化
  • ⚠️ 如果一个类中包含一个或多个抽象方法(abstract method),那么这个类必须声明为抽象类
  • ✅ 抽象类可以同时包含抽象方法和具体方法(concrete method)
  • ⚠️ 继承自抽象类的子类必须实现其所有抽象方法,否则该子类也必须声明为抽象类

我们通过一个简单的例子来加深理解。

定义一个抽象类 BoardGame 来表示棋盘游戏的通用接口:

public abstract class BoardGame {

    //... 字段声明、构造方法等

    public abstract void play();

    //... 具体方法
}

然后定义一个子类 Checkers 实现 play() 方法:

public class Checkers extends BoardGame {

    public void play() {
        //... 具体实现
    }
}

3. 何时使用抽象类

那么,什么时候我们应该优先选择抽象类而不是接口或普通类呢?

以下是一些典型场景:

  • ✅ 需要在多个相关子类中复用某些通用逻辑
  • ✅ 需要部分定义一个 API,让子类可以扩展和细化
  • ✅ 子类需要继承一些带有 protected 访问权限的字段或方法

这些场景都体现了基于继承的 开闭原则(Open/Closed Principle) 的完整实现。

此外,由于抽象类天然支持基类与子类之间的类型关系,我们也能自然地利用 多态(Polymorphism) 特性。

⚠️ 需要注意的是,代码复用虽然是使用抽象类的一个强大理由,但前提是类之间存在“is-a”关系。

✅ 另外,Java 8 引入的默认方法(default method) 有时也可以替代抽象类的部分功能。

4. 文件读取器的示例

为了更直观地理解抽象类的优势,我们来看一个文件读取器的例子。

4.1. 定义抽象基类

假设我们需要实现多种类型的文件读取器,可以先定义一个抽象基类来封装通用逻辑:

public abstract class BaseFileReader {
    
    protected Path filePath;
    
    protected BaseFileReader(Path filePath) {
        this.filePath = filePath;
    }
    
    public Path getFilePath() {
        return 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,方便子类访问。更重要的是,我们将如何处理每一行文本的逻辑留给了子类去实现

虽然 BaseFileReader 看起来不是必须的,但它为整个设计提供了清晰的扩展点,让不同的文件读取器可以专注于自己的业务逻辑

4.2. 定义子类

一个典型的实现是将文件内容转为小写:

public class LowercaseFileReader extends BaseFileReader {

    public LowercaseFileReader(Path filePath) {
        super(filePath);
    }

    @Override
    public String mapFileLine(String line) {
        return line.toLowerCase();
    }   
}

另一个实现可以是将内容转为大写:

public class UppercaseFileReader extends BaseFileReader {

    public UppercaseFileReader(Path filePath) {
        super(filePath);
    }

    @Override
    public String mapFileLine(String line) {
        return line.toUpperCase();
    }
}

从这个例子可以看出,每个子类只需关注自己的行为逻辑,而无需重复处理文件读取的通用流程

4.3. 使用子类

使用继承自抽象类的子类和使用普通类没有区别:

@Test
public void givenLowercaseFileReaderInstance_whenCalledreadFile_thenCorrect() throws Exception {
    URL location = getClass().getClassLoader().getResource("files/test.txt");
    Path path = Paths.get(location.toURI());
    BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);
        
    assertThat(lowercaseFileReader.readFile()).isInstanceOf(List.class);
}

💡 示例中的文件位于 src/main/resources/files 目录下,我们通过类加载器获取其路径。关于类加载器的更多细节,可以参考 Java 类加载器详解

5. 小结

本文简要介绍了 Java 中抽象类的基本用法和适用场景,强调了其在实现抽象、封装通用逻辑方面的优势。

一如既往,文中所有代码均可在 GitHub 项目 中找到。


原始标题:Abstract Classes in Java