抽象类(abstract)
本教程共 100 篇 · 第 44 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
本节目标:理解「抽象方法」和「抽象类」的写法和限制,明白为什么有些类只能当模板、不能 new,并体会「面向抽象编程」带来的松耦合好处。
从一个尴尬的问题说起
前面我们写过 Shape(图形)基类,再派生出 Circle、Rectangle。但仔细想,图形本身到底是什么?
你能不能「画一个图形」?不能。你只能画一个圆、画一个矩形。也就是说,「图形」这个概念太抽象了,它本身没法给出一个具体的 draw() 实现。
可如果在基类里硬写一个 draw(),比如打印一句「画图形」,又容易被子类忘记重写,结果调用到一堆没意义的代码。
这时就该让 draw() 成为一个抽象方法:我规定「所有图形都必须能画」,但具体怎么画,交给子类自己决定。
抽象方法长什么样
在方法签名后面加 abstract 关键字,并且不写方法体(没有大括号,直接以分号结尾):
abstract class Shape {
// 抽象方法:只有声明,没有实现
public abstract void draw();
}
Note抽象方法的核心特征:没有方法体
{},直接以;结束。abstract和「方法体」不能共存。
一旦一个类里出现了抽象方法,这个类必须被声明为抽象类(在 class 前面加 abstract)。反过来,抽象类里不一定非要有抽象方法——一个类即使全是普通方法,也可以被声明成 abstract,目的只是「禁止别人实例化我」。
抽象类不能 new
抽象类最重要的限制:编译器不允许你直接 new 它。
Shape s = new Shape(); // 编译错误!抽象类不能被实例化
原因很直观:抽象方法没有实现,如果让你 new 出来,调用 s.draw() 时 JVM 该执行什么?根本没代码可执行。所以 Java 在编译期就堵死了这条路。
只能 new 它的具体子类:
Shape s = new Circle(); // 正确:Circle 实现了 draw()
s.draw(); // 调用 Circle 的 draw()
子类必须完成「填空」
一个非抽象的子类继承了抽象类,就有义务把父类里所有抽象方法都实现掉,否则它自己也得变成抽象类(再加 abstract)。
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
直接 new 抽象类的报错也很好认:
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 禁止重写,和「必须重写」矛盾
}
编译器的提示很直白:
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();
}
}
运行后输出:
画一个圆
画一个矩形
注意变量类型都声明成 Shape,但真正执行的是各自子类里的实现。这正是利用了上一章讲的多态。
面向抽象编程
抽象类真正的价值,不只是「少写几个方法」,而是让上层代码只依赖抽象约定,不依赖具体实现。
比如我们写一个方法,只认 Shape:
void render(Shape shape) {
shape.draw(); // 不关心到底是圆还是矩形
}
以后不管你新增 Triangle、Star 还是什么图形,只要它继承 Shape 并实现 draw(),render() 一行都不用改就能正常工作。
Note这就是「开闭原则」的雏形:对扩展开放(加子类),对修改关闭(旧的调用代码不动)。抽象类在这里充当了「稳定接口」的角色。
对比一下:如果不用抽象,你很可能写一个 if (shape 是圆) ... else if (shape 是矩形) ... 的大判断,每加一种图形就要回来改这个方法——又回到了复制粘贴、到处改代码的老路。
抽象类 vs 普通父类
很多初学者会问:抽象类和普通父类到底差在哪?一句话记住:
- 普通父类:既能被继承,也能
new自己(虽然通常没人 new)。 - 抽象类:只能被继承,不能
new自己,并且强制子类补全抽象方法。
当你想表达「这是一个不完整的模板,必须有子类来填补细节」时,就用抽象类。
Warning抽象类不能实例化,但可以有构造方法。这个构造方法不是给自己用的,而是给子类的
super()调用链准备的(参见 super 那一章)。不要因为「不能 new」就以为抽象类不能有构造方法。
模板方法:抽象类最经典的用法
抽象类还有一种高频用法:父类把「流程骨架」定死,只把会变的那一两步留成抽象方法。
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 class | interface |
| 数量限制 | 单继承,只能 extends 一个 | 一个类可实现多个 |
| 普通字段 | 可以有 | 只能有 public static final 常量 |
| 构造方法 | 有 | 没有 |
| 方法实现 | 可以写普通方法 | Java 8 起靠 default / static |
| 语义 | 「是一种」(is-a) | 「能干什么」(can-do) |
选择标准只有一条:需要保存对象状态就用抽象类,只是给一份能力约定就用接口。
小结
- 抽象方法用
abstract修饰,没有方法体,以;结尾。 - 含有抽象方法的类必须声明为
abstract类。 - 抽象类不能直接
new,只能new它的具体子类。 - 非抽象子类必须实现父类所有抽象方法,否则自身也要声明为抽象类。
- 抽象类用来表达「模板 + 约定」,是面向抽象编程、降低耦合的基础工具。
到了 Java 8,接口也能通过 default 方法提供默认实现,抽象类不再是「提供公共实现」的唯一选择。下一章我们专门讲接口,看看它和抽象类有什么区别、又该怎么选。