Java 中的抽象类
上次更新:2024 年 1 月 8 日
1. 概述
在实现契约时,我们有时希望将实现的一些部分推迟到以后完成。 我们可以通过抽象类在 Java 中轻松地实现这一点。
在本教程中,我们将学习 Java 中抽象类的基础知识,以及在哪些情况下它们可能有所帮助.
2. 抽象类的关键概念
在深入研究何时使用抽象类之前,让我们看看它们最相关的特性
- 我们使用 abstract 修饰符在 class 关键字前面定义一个抽象类
- 抽象类可以被子类化,但不能被实例化
- 如果一个类定义了一个或多个 abstract 方法,那么该类本身也必须声明为 abstract
- 抽象类可以声明抽象方法和具体方法
- 从抽象类派生的子类必须实现其基类中的所有抽象方法,或者本身也是抽象类
为了更好地理解这些概念,我们将创建一个简单的例子。
让我们让我们的基础抽象类定义一个棋盘游戏的抽象 API
public abstract class BoardGame {
//... field declarations, constructors
public abstract void play();
//... concrete methods
}
然后,我们可以创建一个子类来实现 play 方法
public class Checkers extends BoardGame {
public void play() {
//... implementation
}
}
3. 何时使用抽象类
现在,让我们分析一些典型的场景,在这些场景中,我们应该优先使用抽象类而不是接口和具体类
- 我们希望将一些通用功能封装在一个地方(代码重用),多个相关的子类将共享该功能
- 我们需要部分定义一个 API,以便我们的子类可以轻松扩展和完善它
- 子类需要继承一个或多个具有 protected 访问修饰符的通用方法或字段
请记住,所有这些场景都是完全基于继承的 开放/封闭原则的良好示例。
此外,由于抽象类的使用隐式地处理基本类型和子类型,我们也在利用 多态性。
请注意,只要类层次结构中的“是一种”关系得到保留,代码重用就是一个使用抽象类的非常有力的理由。
而且 Java 8 使用默认方法增加了一个新的因素,有时可以代替创建抽象类。
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 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















