首页 / Java 入门教程 / 抽象类(abstract)

Java 入门教程

抽象类(abstract)

本教程共 100 篇 · 第 44 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

JavaJava 入门教程抽象类abstract抽象方法面向对象

44. 抽象类(abstract)

本节目标:理解「抽象方法」和「抽象类」的写法和限制,明白为什么有些类只能当模板、不能 new,并体会「面向抽象编程」带来的松耦合好处。

从一个尴尬的问题说起

前面我们写过 Shape(图形)基类,再派生出 CircleRectangle。但仔细想,图形本身到底是什么?

你能不能「画一个图形」?不能。你只能画一个圆、画一个矩形。也就是说,「图形」这个概念太抽象了,它本身没法给出一个具体的 draw() 实现。

可如果在基类里硬写一个 draw(),比如打印一句「画图形」,又容易被子类忘记重写,结果调用到一堆没意义的代码。

这时就该让 draw() 成为一个抽象方法:我规定「所有图形都必须能画」,但具体怎么画,交给子类自己决定。

抽象方法长什么样

在方法签名后面加 abstract 关键字,并且不写方法体(没有大括号,直接以分号结尾):

abstract class Shape {
    // 抽象方法:只有声明,没有实现
    public abstract void draw();
}
```bash

> [!NOTE]
> 抽象方法的核心特征:没有方法体 `{}`,直接以 `;` 结束。`abstract` 和「方法体」不能共存。

一旦一个类里出现了抽象方法,这个类**必须**被声明为抽象类(在 `class` 前面加 `abstract`)。反过来,抽象类里**不一定**非要有抽象方法——一个类即使全是普通方法,也可以被声明成 `abstract`,目的只是「禁止别人实例化我」。

## 抽象类不能 new

抽象类最重要的限制:编译器不允许你直接 `new` 它。

```java
Shape s = new Shape();   // 编译错误!抽象类不能被实例化

原因很直观:抽象方法没有实现,如果让你 new 出来,调用 s.draw() 时 JVM 该执行什么?根本没代码可执行。所以 Java 在编译期就堵死了这条路。

只能 new 它的具体子类:

Shape s = new Circle();   // 正确:Circle 实现了 draw()
s.draw();                 // 调用 Circle 的 draw()
```bash

## 子类必须完成「填空」

一个非抽象的子类继承了抽象类,就**有义务把父类里所有抽象方法都实现掉**,否则它自己也得变成抽象类(再加 `abstract`)。

```java
class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("画一个圆");
    }
}

class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("画一个矩形");
    }
}

如果 Circle 漏写了 draw(),编译器会报错:「Circle 不是抽象的,并且没有覆盖 Shape 中的抽象方法 draw()」。

漏写时编译器给的原话是:

error: Circle is not abstract and does not override abstract method draw() in Shape
```text

直接 `new` 抽象类的报错也很好认:

```text
error: Shape is abstract; cannot be instantiated
Tip

给重写的方法加上 @Override,可以让编译器帮你检查:万一方法签名写错(比如拼成 drow),编译器会立刻报错,而不是默默当成新方法。这点我们在方法重写那一章讲过,在抽象类场景下同样好用。

抽象方法的三个「不能」

abstract 和这三个修饰符天生冲突,写在一起直接编译不过:

abstract class Demo {
    private abstract void a();   // 错:private 子类看不见,没法重写
    static  abstract void b();   // 错:static 属于类,不参与重写
    final   abstract void c();   // 错:final 禁止重写,和「必须重写」矛盾
}
```text

编译器的提示很直白:

```text
error: illegal combination of modifiers: abstract and private
error: illegal combination of modifiers: abstract and static
error: illegal combination of modifiers: abstract and final

道理是一致的:抽象方法的唯一出路就是被子类重写,任何挡住重写的修饰符都不能和它同时出现。

完整的可运行示例

把上面的代码凑成一个能跑的程序:

abstract class Shape {
    public abstract void draw();
}

class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("画一个圆");
    }
}

class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("画一个矩形");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape a = new Circle();
        Shape b = new Rectangle();
        a.draw();
        b.draw();
    }
}
```text

运行后输出:

```text
画一个圆
画一个矩形

注意变量类型都声明成 Shape,但真正执行的是各自子类里的实现。这正是利用了上一章讲的多态。

面向抽象编程

抽象类真正的价值,不只是「少写几个方法」,而是让上层代码只依赖抽象约定,不依赖具体实现

比如我们写一个方法,只认 Shape

void render(Shape shape) {
    shape.draw();   // 不关心到底是圆还是矩形
}
```bash

以后不管你新增 `Triangle`、`Star` 还是什么图形,只要它继承 `Shape` 并实现 `draw()`,`render()` 一行都不用改就能正常工作。

> [!NOTE]
> 这就是「开闭原则」的雏形:对扩展开放(加子类),对修改关闭(旧的调用代码不动)。抽象类在这里充当了「稳定接口」的角色。

对比一下:如果不用抽象,你很可能写一个 `if (shape 是圆) ... else if (shape 是矩形) ...` 的大判断,每加一种图形就要回来改这个方法——又回到了复制粘贴、到处改代码的老路。

## 抽象类 vs 普通父类

很多初学者会问:抽象类和普通父类到底差在哪?一句话记住:

- 普通父类:既能被继承,也能 `new` 自己(虽然通常没人 new)。
- 抽象类:**只能被继承,不能 `new` 自己**,并且强制子类补全抽象方法。

当你想表达「这是一个不完整的模板,必须有子类来填补细节」时,就用抽象类。

> [!WARNING]
> 抽象类不能实例化,但**可以有构造方法**。这个构造方法不是给自己用的,而是给子类的 `super()` 调用链准备的(参见 super 那一章)。不要因为「不能 new」就以为抽象类不能有构造方法。

## 模板方法:抽象类最经典的用法

抽象类还有一种高频用法:父类把「流程骨架」定死,只把会变的那一两步留成抽象方法。

```java
abstract class Report {
    // 骨架方法:流程固定,声明为 final 防止子类改乱顺序
    public final void print() {
        header();
        body();          // 只有这一步交给子类
        footer();
    }

    private void header() { System.out.println("=== 报表开头 ==="); }
    private void footer() { System.out.println("=== 报表结尾 ==="); }

    protected abstract void body();
}

子类只需要操心 body() 怎么写,开头结尾的顺序永远不会被改乱。这套思路叫模板方法,是抽象类最能体现价值的场景之一。这里只点到概念,具体设计模式不在本教程范围内。

抽象类 vs 接口速查

下一章要讲接口,先把两者的差异摆清楚,学接口时会轻松很多:

对比项抽象类接口
关键字abstract classinterface
数量限制单继承,只能 extends 一个一个类可实现多个
普通字段可以有只能有 public static final 常量
构造方法没有
方法实现可以写普通方法Java 8 起靠 default / static
语义「是一种」(is-a)「能干什么」(can-do)

选择标准只有一条:需要保存对象状态就用抽象类,只是给一份能力约定就用接口。

小结

  • 抽象方法用 abstract 修饰,没有方法体,以 ; 结尾。
  • 含有抽象方法的类必须声明为 abstract 类。
  • 抽象类不能直接 new,只能 new 它的具体子类。
  • 非抽象子类必须实现父类所有抽象方法,否则自身也要声明为抽象类。
  • 抽象类用来表达「模板 + 约定」,是面向抽象编程、降低耦合的基础工具。

到了 Java 8,接口也能通过 default 方法提供默认实现,抽象类不再是「提供公共实现」的唯一选择。下一章我们专门讲接口,看看它和抽象类有什么区别、又该怎么选。