【Java】抽象类 多态的上层基础
🏛️ Java 抽象类详解
📌 抽象类是 Java 面向对象的重要机制,是多态的上层基础
理解抽象类,就理解了"如何用编译器强制规范子类行为"
📖 本文导读
本文系统讲解 Java 抽象类的概念、语法、8大特性、继承链、与多态结合、内存模型、模板方法模式、常见陷阱与面试题。适合已掌握继承和方法重写的读者
文章目录
1️⃣ 抽象类的概念 🤔
1.1 先看一个场景–图形绘制
假设我们要画不同的图形,每种图形都有一个 draw() 方法:
// 父类 Shape
public class Shape {
public void draw() {
System.out.println("画一个图形...");
}
}
// 子类 Cycle(圆形)
public class Cycle extends Shape {
@Override
public void draw() {
System.out.println("画一个圆圈..."); // ●
}
}
// 子类 Rect(矩形)
public class Rect extends Shape {
@Override
public void draw() {
System.out.println("画一个矩形..."); // ■
}
}
// 子类 Flower(花)
public class Flower extends Shape {
@Override
public void draw() {
System.out.println("画一朵花..."); // ❀
}
}
然后我们用多态来画图:
public class Test {
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
drawMap(new Cycle()); // 画一个圆圈...
drawMap(new Rect()); // 画一个矩形...
drawMap(new Flower()); // 画一朵花...
}
}
看起来没问题?但仔细想想–Shape 的 draw() 方法根本就没干实事!它只是输出了"画一个图形…",而真正绘制图形的工作全是由子类完成的。
⚠️ 问题来了: 如果有人不小心直接
new Shape()并调用draw(),打印出"画一个图形…“–这毫无意义!Shape 本身就是一种"概念”,不是具体的东西
1.2 再看一个场景–动物叫声
public class Animal {
public void cry() {
System.out.println("动物发出叫声..."); // 这句话毫无实际意义
}
}
“动物发出叫声”–到底是汪汪汪还是喵喵喵?Animal 本身就不能确定叫声是什么。每个具体的动物才知道自己怎么叫。
1.3 再看一个场景–交通工具
public class Vehicle {
public void fuelConsumption() {
// 油耗?电动车根本不烧油!
// 这个方法对 Vehicle 来说完全无法给出合理实现
}
}
电动车、燃油车、混合动力车的油耗计算方式完全不同,父类 Vehicle 根本写不出一个通用的 fuelConsumption() 实现。
1.4 什么是抽象类?
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的。如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
| 概念 | 具体还是抽象? | 能否实例化? |
|---|---|---|
| 🐕 Dog(狗) | 具体 | ✅ 可以 new Dog() |
| 🐱 Cat(猫) | 具体 | ✅ 可以 new Cat() |
| 🐾 Animal(动物) | 抽象 | ❌ 不应该 new Animal() |
| ● Cycle(圆形) | 具体 | ✅ 可以 new Cycle() |
| 📐 Shape(图形) | 抽象 | ❌ 不应该 new Shape() |
| 🚗 Car(汽车) | 具体 | ✅ 可以 new Car() |
| 🚙 Vehicle(交通工具) | 抽象 | ❌ 不应该 new Vehicle() |
抽象方法:没有实际工作的方法,只有方法声明,没有方法体
抽象类:包含抽象方法的类(或者用 abstract 修饰的类)
💡 把 Shape 的
draw()设计成抽象方法,Shape 设计成抽象类,这样编译器就能帮你禁止直接实例化 Shape,强制子类必须重写draw()方法
2️⃣ 为什么需要抽象类?🔑
2.1 普通类也能被继承,为啥非要用抽象类?
确实,普通类也能被继承,普通方法也能被重写。但使用抽象类相当于多了一重编译器的校验:
// 场景:实际工作不应该由父类完成,而应由子类完成
// ❌ 用普通类:不小心误用父类,编译器不会报错
public class Shape {
public void draw() {
System.out.println("画一个图形..."); // 这根本就不该执行!
}
}
Shape s = new Shape(); // 编译通过,但运行结果毫无意义
s.draw(); // 输出"画一个图形..." -- 瞎画的!
// 更糟糕的是:新增子类时忘了重写 draw(),编译器也不会提醒你
// ✅ 用抽象类:不小心误用父类,编译器立即报错
public abstract class Shape {
abstract public void draw(); // 强制子类实现
}
Shape s = new Shape(); // ❌ 编译报错!Shape是抽象的; 无法实例化
// 新增子类忘了重写 draw() → 编译报错!必须重写抽象方法
2.2 编译器校验的价值
🔑 核心意义:抽象类的存在是为了"预防出错"
很多语法存在的意义都是为了"预防出错":
语法 防止什么错误? final防止不小心修改不该修改的类/方法/变量 abstract防止不小心实例化不该实例化的类,或者忘记重写该重写的方法 private防止外部误访问内部实现 @Override防止拼写错误导致方法名不对(不是重写而是重载) 充分利用编译器的校验,在实际开发中是非常有意义的! bug 越早发现越好,编译期能发现的,绝不拖到运行期
2.3 抽象类的设计哲学
抽象类表达的是"is-a"关系 – “圆形是一种图形”,“狗是一种动物”
它的设计哲学是:
- 提取共性 → 把所有子类共有的属性和行为放到抽象类中
- 声明规范 → 把"必须由子类实现"的方法声明为抽象方法
- 禁止误用 → 编译器强制检查,不允许实例化抽象类
3️⃣ 抽象类的语法 📝
3.1 定义抽象类
在 Java 中,用 abstract 关键字修饰:
// 抽象类:被 abstract 修饰的类
public abstract class Shape {
// 抽象方法:被 abstract 修饰的方法,没有方法体(没有大括号)
abstract public void draw();
// 抽象类也是类,也可以有普通方法和属性
public double getArea() {
return area;
}
protected double area; // 面积
}
🔑 语法要点:
abstract class 类名→ 定义抽象类abstract 返回值 方法名();→ 定义抽象方法(没有方法体,直接分号结束)- 抽象类中可以有普通方法和属性,甚至构造方法
- 抽象方法必须在抽象类中(有抽象方法的类一定是抽象类)
abstract不能和final、private、static同时修饰方法
3.2 子类继承抽象类
子类继承抽象类后,必须重写所有抽象方法,否则子类也得是抽象类:
// ✅ 正确:重写了所有抽象方法
public class Cycle extends Shape {
@Override
public void draw() {
System.out.println("画一个圆圈...");
}
}
// ✅ 正确:重写了所有抽象方法
public class Rect extends Shape {
@Override
public void draw() {
System.out.println("画一个矩形...");
}
}
// ✅ 正确:重写了所有抽象方法
public class Flower extends Shape {
@Override
public void draw() {
System.out.println("画一朵花...");
}
}
[!TIP]
💡 IDEA 中按
Ctrl + I可以快速实现抽象方法,或按Alt + Enter选择 “Implement methods”
3.3 使用抽象类实现多态
public class Test {
public static void drawMap(Shape shape) {
shape.draw(); // 多态:具体调用哪个 draw(),看运行时类型
}
public static void main(String[] args) {
// Shape shape = new Shape(); // ❌ 编译错误!抽象类不能实例化
Shape shape = new Cycle(); // ✅ 向上转型
drawMap(shape); // 画一个圆圈...
drawMap(new Cycle()); // 画一个圆圈...
drawMap(new Flower()); // 画一朵花...
drawMap(new Rect()); // 画一个矩形...
// 抽象类数组
Shape[] shapes = {new Cycle(), new Rect(), new Flower()};
for (Shape s : shapes) {
s.draw(); // 多态遍历
}
}
}
3.4 语法规则速查表
| 规则 | 说明 | 示例 |
|---|---|---|
| 类用 abstract 修饰 | 抽象类定义 | abstract class Shape {} |
| 方法用 abstract 修饰 | 抽象方法,无方法体 | abstract void draw(); |
| 有抽象方法→类必为抽象 | 编译器强制 | 含 abstract 方法的类不加 abstract → 报错 |
| 子类必须重写所有抽象方法 | 否则子类也要声明 abstract | 详见 4.4 节 |
| 抽象类不能实例化 | new 抽象类() 编译报错 |
详见 4.1 节 |
4️⃣ 抽象类的8大特性 🔍
4.1 特性一:抽象类不能直接实例化对象 🚫
Shape shape = new Shape(); // ❌ 编译出错!
// Error: Shape是抽象的; 无法实例化
⚠️ 抽象类不能
new,但可以声明引用,指向子类对象(向上转型)Shape shape = new Cycle(); // ✅ 这是可以的
为什么不能实例化? 抽象类是不完整的类,里面有没有实现的方法。如果允许实例化,调用到未实现的抽象方法怎么办?
4.2 特性二:抽象方法不能是 private 的 🔒
abstract class Shape {
abstract private void draw(); // ❌ 编译出错!
}
// Error: 非法的修饰符组合: abstract和private
🔑 原因: 抽象方法就是为了让子类重写的!如果是
private,子类根本看不见,还怎么重写?抽象方法的访问权限必须是子类可见的:
public或protected或默认(包权限)
4.3 特性三:抽象方法不能被 final 和 static 修饰 🚫
public abstract class Shape {
abstract final void methodA(); // ❌ 编译出错!
abstract public static void methodB(); // ❌ 编译出错!
}
// Error: 非法的修饰符组合: abstract和final
// Error: 非法的修饰符组合: abstract和static
🔑 原因:
final→ 方法不能被重写 → 和 abstract(要求被重写)直接矛盾static→ 方法属于类,不参与多态,通过类名调用 → 和 abstract(要求子类通过对象重写)直接矛盾
修饰符 含义 和 abstract 矛盾的原因 private子类不可见 抽象方法需要子类重写,但子类看不到 final不可重写 抽象方法必须被重写,但 final 禁止重写 static属于类,不参与多态 抽象方法需要子类通过对象重写,static 不需要对象
4.4 特性四:子类必须重写所有抽象方法 📋
继承抽象类后,子类要么重写所有抽象方法,要么自己也声明为抽象类:
public abstract class Shape {
abstract public void draw();
abstract public double calcArea();
}
// 方案A:✅ 重写所有抽象方法 → 变成具体类
public class Cycle extends Shape {
private double r;
public Cycle(double r) { this.r = r; }
@Override
public void draw() {
System.out.println("画一个圆圈,半径=" + r);
}
@Override
public double calcArea() {
return Math.PI * r * r;
}
}
// 方案B:✅ 只重写一部分 → 自己也要声明为抽象类
public abstract class Triangle extends Shape {
private double a, b, c;
@Override
public void draw() {
System.out.println("画一个三角形");
}
// 没有重写 calcArea(),所以 Triangle 必须也是 abstract 的
}
// 方案C:✅ 抽象类继承抽象类 → 可以一个都不重写
public abstract class Polygon extends Shape {
protected int sides;
// 一个抽象方法都没重写也行!因为 Polygon 本身也是抽象的
}
💡 这就是抽象类的链式传递:父类的抽象方法可以"传递"给子类,直到某个具体的子类把它们全部实现
就像接力赛:抽象方法是一根接力棒,从抽象父类 → 抽象子类 → … → 具体子类,直到有人(具体类)接住并完成
4.5 特性五:抽象类中不一定包含抽象方法 💭
// 这个抽象类没有任何抽象方法,但仍然是抽象类
public abstract class Animal {
public void eat() {
System.out.println("吃东西...");
}
public void sleep() {
System.out.println("睡觉...");
}
// 没有抽象方法也可以!
}
📌 关键结论:
- 有抽象方法 → 一定是抽象类 ✅
- 抽象类 → 不一定有抽象方法 ✅
没有抽象方法的抽象类有什么用?
- 禁止实例化 → 作为"标记",表示这个类不应该被直接使用
- 为将来扩展预留 → 后续可能需要添加抽象方法
- JDK 中的例子:
java.awt.Component的某些子类、javax.servlet.GenericServlet
4.6 特性六:抽象类中可以有构造方法 🏗️
public abstract class Shape {
protected String color;
protected double area;
// 抽象类的构造方法:供子类创建对象时初始化父类的成员变量
public Shape(String color) {
this.color = color;
System.out.println("Shape 构造方法被调用,color=" + color);
}
abstract public void draw();
}
public class Cycle extends Shape {
private double r;
public Cycle(String color, double r) {
super(color); // 调用父类 Shape 的构造方法
this.r = r;
}
@Override
public void draw() {
System.out.println("画一个" + color + "的圆圈,半径=" + r);
}
}
public class Test {
public static void main(String[] args) {
Cycle c = new Cycle("红色", 5.0);
c.draw(); // 画一个红色的圆圈,半径=5.0
}
}
// 输出:
// Shape 构造方法被调用,color=红色
// 画一个红色的圆圈,半径=5.0
🔑 抽象类的构造方法不是给自己用的(自己不能实例化),而是给子类用的
子类构造方法中通过
super()调用父类构造方法,初始化从父类继承的成员变量如果不显式写
super(...),编译器会自动加super()(调用无参构造)
4.7 特性七:抽象类可以有普通成员 ✅
public abstract class Shape {
// ✅ 普通属性
protected String color = "白色";
protected double area;
private static int count = 0; // 静态属性也可以
// ✅ 普通方法(有方法体的方法)
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
// ✅ 静态方法
public static int getCount() {
return count;
}
// ✅ 最终方法(子类不能重写,但可以继承使用)
public final void printInfo() {
System.out.println("颜色:" + color + ",面积:" + area);
}
// 抽象方法
abstract public void draw();
abstract public double calcArea();
}
💡 抽象类 = 普通类 + 抽象方法。抽象类能做的事,普通类几乎都能做,除了:不能实例化、能有抽象方法
4.8 特性八:抽象类可以进行向上转型 🔼
public abstract class Shape {
abstract public void draw();
protected String color = "白色";
}
public class Cycle extends Shape {
@Override
public void draw() {
System.out.println("画圆圈");
}
}
public class Test {
public static void main(String[] args) {
// ✅ 向上转型:抽象类引用指向具体子类对象
Shape shape = new Cycle();
shape.draw(); // 画圆圈(多态)
shape.color; // 白色(访问父类成员)
// ✅ 作为方法参数
drawMap(new Cycle()); // 画圆圈
}
public static void drawMap(Shape shape) {
shape.draw(); // 多态调用
}
}
4.9 特性完整总结表 📊
| 序号 | 特性 | 说明 | 违反后果 |
|---|---|---|---|
| 1 | 不能实例化 | new 抽象类() 编译报错 |
编译错误 |
| 2 | 抽象方法不能是 private | private 和 abstract 矛盾 | 编译错误:非法修饰符组合 |
| 3 | 抽象方法不能是 final/static | 它们和 abstract 语义冲突 | 编译错误:非法修饰符组合 |
| 4 | 子类必须重写所有抽象方法 | 否则子类也要声明为抽象类 | 编译错误:子类未实现抽象方法 |
| 5 | 可以没有抽象方法 | 但有抽象方法一定是抽象类 | - |
| 6 | 可以有构造方法 | 供子类通过 super() 调用 | - |
| 7 | 可以有普通成员 | 普通方法、属性、静态成员都可以 | - |
| 8 | 可以进行向上转型 | 抽象类 引用 = new 具体子类(); |
- |
5️⃣ 抽象类的继承链 🔗
5.1 多层抽象类的传递
抽象方法可以从祖先类一直"传递"到后代类,直到某个具体类把它们全部实现:

代码实现:
// 第一层:最顶层的抽象类
public abstract class Shape {
protected String color;
protected double area;
public Shape(String color) {
this.color = color;
}
abstract public void draw();
abstract public double calcArea();
// 具体方法:所有子类通用
public final void printInfo() {
System.out.println("颜色=" + color + ",面积=" + calcArea());
}
}
// 第二层:中间的抽象类(部分实现)
public abstract class Polygon extends Shape {
protected int sides; // 边数
public Polygon(String color, int sides) {
super(color);
this.sides = sides;
}
@Override
public void draw() {
System.out.println("画一个" + sides + "边形,颜色=" + color);
}
// calcArea() 仍然没有实现 → Polygon 也是抽象的
}
// 第三层:具体类(全部实现)
public class Triangle extends Polygon {
private double a, b, c;
public Triangle(String color, double a, double b, double c) {
super(color, 3);
this.a = a;
this.b = b;
this.c = c;
}
@Override
public double calcArea() {
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c)); // 海伦公式
}
}
// 第三层:另一个具体类
public class Rectangle extends Polygon {
private double width, height;
public Rectangle(String color, double width, double height) {
super(color, 4);
this.width = width;
this.height = height;
}
@Override
public double calcArea() {
return width * height;
}
}
5.2 继承链的构造方法调用顺序
public class Test {
public static void main(String[] args) {
Triangle t = new Triangle("蓝色", 3, 4, 5);
t.draw(); // 画一个3边形,颜色=蓝色
t.printInfo(); // 颜色=蓝色,面积=6.0
}
}
构造顺序:
1. new Triangle("蓝色", 3, 4, 5)
2. → Triangle 构造 → super("蓝色", 3)
3. → Polygon 构造 → super("蓝色")
4. → Shape 构造 → 初始化 color, area
5. ← Shape 构造完成
6. ← Polygon 构造完成,初始化 sides
7. ← Triangle 构造完成,初始化 a, b, c
💡 构造方法调用始终从最顶层开始,逐层往下。虽然抽象类不能实例化,但它的构造方法在子类实例化时一定会被调用
6️⃣ 抽象类与多态的结合 🎯
6.1 抽象类天生就是为多态准备的
抽象类不能实例化 → 只能通过子类对象来使用 → 自然就用到了向上转型 → 多态!
public abstract class Shape {
protected String color;
abstract public void draw();
abstract public double calcArea();
}
public class Cycle extends Shape {
private double r;
public Cycle(double r) { this.r = r; }
@Override
public void draw() { System.out.println("●"); }
@Override
public double calcArea() { return Math.PI * r * r; }
}
public class Rect extends Shape {
private double w, h;
public Rect(double w, double h) { this.w = w; this.h = h; }
@Override
public void draw() { System.out.println("■"); }
@Override
public double calcArea() { return w * h; }
}
6.2 多态方法调用的完整流程
public class Test {
// 抽象类引用作为方法参数 → 多态的典型应用
public static void drawMap(Shape shape) {
shape.draw(); // 多态:运行时决定调用哪个 draw()
}
public static double totalArea(Shape[] shapes) {
double sum = 0;
for (Shape s : shapes) {
sum += s.calcArea(); // 多态:每种图形调用自己的 calcArea()
}
return sum;
}
public static void main(String[] args) {
Shape[] shapes = {new Cycle(3), new Rect(4, 5), new Cycle(2)};
// 1. 绘制所有图形
for (Shape s : shapes) {
drawMap(s);
}
// ●
// ■
// ●
// 2. 计算总面积
System.out.println("总面积:" + totalArea(shapes));
// 总面积:40.27433388230814
// 3. 向下转型访问子类独有方法
for (Shape s : shapes) {
if (s instanceof Cycle c) {
System.out.println("圆的半径:" + c.r); // 访问 Cycle 独有属性
} else if (s instanceof Rect r) {
System.out.println("矩形的宽高:" + r.w + "x" + r.h);
}
}
}
}
6.3 多态调用的编译时和运行时
🔑 编译时 vs 运行时:
阶段 检查内容 结果 编译时 shape.draw()→ Shape 中有没有draw()方法?有(抽象方法也算),编译通过 运行时 shape 指向的实际对象是什么类型? Cycle → 调用 Cycle.draw() 这就是动态绑定:编译时只检查方法是否存在,运行时才决定调用哪个版本
7️⃣ 抽象类的内存模型 🧠
7.1 抽象类引用指向子类对象的内存图
Shape shape = new Cycle(5.0);
shape.draw();

7.2 调用 shape.draw() 的动态绑定过程
1. 编译阶段:
shape 的编译类型是 Shape
→ 编译器检查 Shape 中有没有 draw() 方法
→ 有(虽然是抽象的)→ 编译通过 ✅
2. 运行阶段:
shape 指向堆中的 Cycle 对象
→ JVM 从该对象的方法表中查找 draw()
→ 找到 Cycle 重写的 draw() 方法的地址
→ 跳转执行 Cycle.draw() → 输出 "●"
3. 关键点:
shape 引用类型是 Shape → 决定了能调用哪些方法(编译时检查)
shape 实际对象是 Cycle → 决定了调用哪个版本的方法(运行时绑定)
7.3 抽象类数组的内存图
Shape[] shapes = {new Cycle(3), new Rect(4, 5), new Cycle(2)};

💡 抽象类数组的每个元素都是抽象类引用,但实际指向的是具体子类对象。遍历时通过动态绑定,每种对象调用自己的方法实现
8️⃣ 模板方法模式 🎨
8.1 什么是模板方法模式?
模板方法模式(Template Method Pattern) 是抽象类最经典的应用,属于行为型设计模式。
核心思想:在抽象类中定义一个"模板方法"(算法骨架),把某些步骤声明为抽象方法,由子类来具体实现。
📌 生活中的模板:
🍳 炒菜流程:热锅 → 倒油 → 下食材 → 翻炒 → 调味 → 出锅
- "下食材"和"调味"每道菜不同 → 抽象方法
- 其他步骤通用 → 具体方法
🏭 制造流程:原材料入库 → 加工 → 质检 → 包装 → 出库
- "加工"和"包装"每种产品不同 → 抽象方法
8.2 完整示例:饮料制作流程
// 抽象类:饮料(定义制作模板)
public abstract class Beverage {
// 模板方法:定义制作流程的骨架(final 防止子类重写)
public final void make() {
boilWater(); // 1. 烧水
brew(); // 2. 冲泡(抽象,子类实现)
pourInCup(); // 3. 倒入杯中
addCondiments(); // 4. 加调料(抽象,子类实现)
System.out.println("☕ 饮料制作完成!");
System.out.println("----------------------");
}
// 具体方法:所有饮料通用
private void boilWater() {
System.out.println("1. 烧开水");
}
// 抽象方法:每种饮料冲泡方式不同
protected abstract void brew();
// 具体方法:所有饮料通用
private void pourInCup() {
System.out.println("3. 倒入杯中");
}
// 抽象方法:每种饮料加的调料不同
protected abstract void addCondiments();
}
// 具体类:咖啡
public class Coffee extends Beverage {
@Override
protected void brew() {
System.out.println("2. 用沸水冲泡咖啡粉");
}
@Override
protected void addCondiments() {
System.out.println("4. 加糖和牛奶");
}
}
// 具体类:茶
public class Tea extends Beverage {
@Override
protected void brew() {
System.out.println("2. 用沸水浸泡茶叶");
}
@Override
protected void addCondiments() {
System.out.println("4. 加柠檬片");
}
}
// 具体类:奶茶
public class MilkTea extends Beverage {
@Override
protected void brew() {
System.out.println("2. 煮茶汤并加入鲜奶");
}
@Override
protected void addCondiments() {
System.out.println("4. 加珍珠和椰果");
}
}
// 测试
public class Test {
public static void main(String[] args) {
Beverage coffee = new Coffee();
coffee.make();
// 1. 烧开水
// 2. 用沸水冲泡咖啡粉
// 3. 倒入杯中
// 4. 加糖和牛奶
// ☕ 饮料制作完成!
Beverage tea = new Tea();
tea.make();
// 1. 烧开水
// 2. 用沸水浸泡茶叶
// 3. 倒入杯中
// 4. 加柠檬片
// ☕ 饮料制作完成!
// 多态:统一处理
Beverage[] beverages = {new Coffee(), new Tea(), new MilkTea()};
for (Beverage b : beverages) {
b.make(); // 每种饮料调用自己的 brew() 和 addCondiments()
}
}
}
8.3 模板方法模式的结构

🔑 模板方法模式的三要素:
- 模板方法:
final修饰,定义算法骨架,子类不能改变流程- 具体方法:通用步骤,子类直接继承使用
- 抽象方法:变化步骤,子类必须实现
8.4 另一个示例:游戏角色模板
public abstract class GameCharacter {
// 模板方法:角色的战斗流程
public final void fight() {
enterBattle(); // 进入战斗
attack(); // 攻击(抽象)
useSkill(); // 使用技能(抽象)
celebrate(); // 庆祝胜利
}
private void enterBattle() {
System.out.println("⚔️ 进入战斗!");
}
protected abstract void attack();
protected abstract void useSkill();
private void celebrate() {
System.out.println("🎉 胜利!");
}
}
public class Warrior extends GameCharacter {
@Override
protected void attack() {
System.out.println("🗡️ 挥舞大剑攻击");
}
@Override
protected void useSkill() {
System.out.println("🛡️ 释放护盾技能");
}
}
public class Mage extends GameCharacter {
@Override
protected void attack() {
System.out.println("🔮 发射魔法飞弹");
}
@Override
protected void useSkill() {
System.out.println("☄️ 释放火球术");
}
}
9️⃣ 抽象类 vs 普通父类 🆚
9.1 完整对比表
| 对比项 | 普通父类 | 抽象类 |
|---|---|---|
| 能否实例化 | ✅ 可以 | ❌ 不可以 |
| 能否有抽象方法 | ❌ 不可以 | ✅ 可以 |
| 子类是否必须重写 | 可选 | 必须重写所有抽象方法 |
| 编译器校验 | 弱(忘记重写不会报错) | 强(忘记重写直接报错) |
| 设计目的 | 代码复用 | 规范子类行为 + 代码复用 |
| 适用场景 | 通用功能抽取,子类可选择性重写 | 强制子类必须实现某些方法 |
| 能否有构造方法 | ✅ 可以 | ✅ 可以 |
| 能否有 final 方法 | ✅ 可以 | ✅ 可以 |
9.2 何时用普通父类?何时用抽象类?
🔑 选择原则:
- 如果父类的方法有合理的默认实现,子类可以选择性重写 → 用普通父类
- 如果父类的方法没有合理实现,子类必须重写 → 用抽象类
- 不确定?→ 用抽象类!多一层编译器校验总是好的
🔟 抽象类 vs 接口 🆚
10.1 核心区别
🔑 核心区别: 抽象类表达 is-a 关系(“是什么”),接口表达 has-ability 关系(“能做什么”)
10.2 完整对比表
| 对比项 | 抽象类 | 接口 |
|---|---|---|
| 关键字 | abstract class |
interface |
| 实例化 | ❌ 不能 | ❌ 不能 |
| 构造方法 | ✅ 可以有 | ❌ 不能有 |
| 普通成员变量 | ✅ 可以有 | ❌ 只有 public static final 常量 |
| 普通方法 | ✅ 可以有 | ❌ 只有 default/static 方法(Java 8+) |
| 抽象方法 | ✅ 可以有 | ✅ 默认都是 |
| static 代码块 | ✅ 可以有 | ❌ 不能有 |
| 访问修饰符 | 任意 | 方法默认 public |
| 继承/实现 | 单继承(extends) |
多实现(implements) |
| 设计目的 | is-a 关系(是什么) | has-ability 关系(能做什么) |
| 典型代表 | Animal、Shape、Vehicle |
Runnable、Comparable、Cloneable |
💡 下一篇我们将深入学习接口,届时会有更详细的对比和应用
1️⃣1️⃣ 常见错误与陷阱 ⚠️
11.1 abstract 和 final 同时修饰类
// ❌ 错误:抽象类要求被继承,final 禁止被继承,矛盾!
public abstract final class Shape { }
// Error: 非法的修饰符组合: abstract和final
11.2 忘记重写抽象方法
public abstract class Shape {
abstract public void draw();
abstract public double calcArea();
}
// ❌ 只重写了一个,另一个没重写
public class Cycle extends Shape {
@Override
public void draw() {
System.out.println("画圆");
}
// 缺少 calcArea() → 编译报错!
// Error: Cycle不是抽象的,并且未覆盖Shape中的抽象方法calcArea()
}
11.3 抽象方法有方法体
public abstract class Shape {
// ❌ 抽象方法不能有方法体
abstract public void draw() {
System.out.println("...");
}
// Error: 抽象方法不能有主体
}
// ✅ 正确写法:abstract public void draw(); // 直接分号,没有大括号
11.4 在非抽象类中有抽象方法
// ❌ 没有abstract修饰,但有抽象方法
public class Shape {
abstract public void draw();
// Error: Shape不是抽象的,并且未覆盖抽象方法draw()
}
// ✅ 正确:public abstract class Shape { ... }
11.5 用抽象类引用调用子类独有方法
public abstract class Shape {
abstract public void draw();
}
public class Cycle extends Shape {
public double r;
@Override
public void draw() { System.out.println("●"); }
public void printRadius() { System.out.println("半径=" + r); }
}
Shape shape = new Cycle();
shape.printRadius(); // ❌ 编译错误!Shape 中没有 printRadius()
// ✅ 需要向下转型
if (shape instanceof Cycle c) {
c.printRadius();
}
11.6 抽象类中调用抽象方法(this.抽象方法)
public abstract class Shape {
abstract public void draw();
// ✅ 这是合法的!在具体方法中调用抽象方法
public void show() {
System.out.println("准备绘制...");
this.draw(); // 运行时会调用子类重写的版本(多态)
System.out.println("绘制完成!");
}
}
public class Cycle extends Shape {
@Override
public void draw() {
System.out.println("画圆圈");
}
}
// 测试
Shape s = new Cycle();
s.show();
// 准备绘制...
// 画圆圈 ← 调用的是 Cycle 的 draw()
// 绘制完成!
💡 这其实就是模板方法模式的基础!在抽象类的具体方法中调用抽象方法,由子类提供具体实现
1️⃣2️⃣ 面试题精选 🎯
题目1:抽象类和普通类的区别?
答:
区别 普通类 抽象类 实例化 可以 不可以 抽象方法 不能有 可以有 子类重写 可选 必须重写所有抽象方法(否则子类也为抽象类) 修饰符 无限制 必须有 abstract本质区别:抽象类比普通类多了一层编译器校验,强制子类实现特定方法
题目2:抽象类必须有抽象方法吗?
答: 不必须。抽象类可以没有抽象方法,但仍然不能被实例化。没有抽象方法的抽象类通常作为"标记"使用,表示这个类不应该被直接使用。
反过来,有抽象方法的类必须是抽象类。
题目3:抽象类能用 final 修饰吗?
答: 不能。
abstract表示该类需要被继承,final表示该类不能被继承,两者语义矛盾,编译器会报错:“非法的修饰符组合: abstract和final”。
题目4:下面代码能编译通过吗?
public abstract class A {
abstract private void method();
}
答: 不能。
abstract和private不能同时修饰方法。抽象方法的目的是让子类重写,而private方法对子类不可见,无法重写,两者矛盾。
题目5:抽象类的构造方法有什么用?既然不能实例化?
答: 抽象类的构造方法是给子类调用的。子类实例化时,会通过
super()调用父类(抽象类)的构造方法来初始化从父类继承的成员变量。虽然抽象类本身不能new,但子类new时一定会先执行父类的构造方法。
题目6:下面代码的输出是什么?
public abstract class Base {
public Base() {
System.out.println("Base构造");
show(); // 调用抽象方法!
}
public abstract void show();
}
public class Derived extends Base {
private int num = 10;
public Derived() {
super();
System.out.println("Derived构造");
}
@Override
public void show() {
System.out.println("num = " + num);
}
}
public class Test {
public static void main(String[] args) {
new Derived();
}
}
答: 输出:
Base构造 num = 0 ← 注意!不是 10! Derived构造原因: 父类构造方法中调用
show()时,由于多态,实际执行的是Derived.show()。但此时Derived的成员变量num还未被初始化(还处于默认值 0),因为初始化顺序是:父类构造 → 子类成员变量初始化 → 子类构造方法体。教训: 不要在构造方法中调用可被重写的方法(包括抽象方法)!
1️⃣3️⃣ 完整实战示例 – 员工薪资系统 💼
13.1 需求描述
一个公司有三种员工:
- 正式员工(FullTimeEmployee):固定月薪 + 奖金
- 兼职员工(PartTimeEmployee):时薪 × 工时
- 实习生(Intern):固定实习津贴
要求用抽象类统一管理,计算薪资并输出工资条。
13.2 代码实现
// 抽象类:员工
public abstract class Employee {
protected String name;
protected int id;
protected String department;
public Employee(String name, int id, String department) {
this.name = name;
this.id = id;
this.department = department;
}
// 抽象方法:计算薪资(每种员工计算方式不同)
public abstract double calculateSalary();
// 具体方法:打印工资条(所有员工通用)
public final void printPaySlip() {
System.out.println("------- 员工工资条 -------");
System.out.println("姓名: " + name);
System.out.println("工号: " + id);
System.out.println("部门: " + department);
System.out.println("员工类型: " + this.getClass().getSimpleName());
System.out.println("实发薪资: " + String.format("%.2f", calculateSalary()) + " 元");
System.out.println("------------------------");
System.out.println();
}
// 具体方法:工作(所有员工通用)
public void work() {
System.out.println(name + "正在" + department + "工作");
}
}
// 正式员工
public class FullTimeEmployee extends Employee {
private double monthlySalary; // 月薪
private double bonus; // 奖金
public FullTimeEmployee(String name, int id, String department,
double monthlySalary, double bonus) {
super(name, id, department);
this.monthlySalary = monthlySalary;
this.bonus = bonus;
}
@Override
public double calculateSalary() {
return monthlySalary + bonus;
}
}
// 兼职员工
public class PartTimeEmployee extends Employee {
private double hourlyRate; // 时薪
private int hours; // 工时
public PartTimeEmployee(String name, int id, String department,
double hourlyRate, int hours) {
super(name, id, department);
this.hourlyRate = hourlyRate;
this.hours = hours;
}
@Override
public double calculateSalary() {
return hourlyRate * hours;
}
}
// 实习生
public class Intern extends Employee {
private double allowance; // 实习津贴
public Intern(String name, int id, String department, double allowance) {
super(name, id, department);
this.allowance = allowance;
}
@Override
public double calculateSalary() {
return allowance;
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// 创建不同类型的员工
Employee[] employees = {
new FullTimeEmployee("张三", 1001, "研发部", 15000, 5000),
new PartTimeEmployee("李四", 2001, "市场部", 80, 60),
new Intern("王五", 3001, "人事部", 3000)
};
// 统一处理:打印工资条
System.out.println("===== 本月工资发放 =====\n");
double totalSalary = 0;
for (Employee emp : employees) {
emp.work(); // 多态
emp.printPaySlip(); // 多态(printPaySlip 是 final,但内部调用了 calculateSalary 多态)
totalSalary += emp.calculateSalary();
}
System.out.println("===== 公司本月薪资总支出:" + String.format("%.2f", totalSalary) + " 元 =====");
}
}
13.3 运行结果
===== 本月工资发放 =====
张三正在研发部工作
------- 员工工资条 -------
姓名: 张三
工号: 1001
部门: 研发部
员工类型: FullTimeEmployee
实发薪资: 20000.00 元
------------------------
李四正在市场部工作
------- 员工工资条 -------
姓名: 李四
工号: 2001
部门: 市场部
员工类型: PartTimeEmployee
实发薪资: 4800.00 元
------------------------
王五正在人事部工作
------- 员工工资条 -------
姓名: 王五
工号: 3001
部门: 人事部
员工类型: Intern
实发薪资: 3000.00 元
------------------------
===== 公司本月薪资总支出:27800.00 元 =====
💡 这个示例完美展示了抽象类的核心价值:
- 抽象方法
calculateSalary()→ 强制每种员工必须实现自己的薪资计算- 具体方法
printPaySlip()→ 所有员工共享工资条打印逻辑- 多态 + 抽象类 → 统一遍历处理,无需关心具体类型
- final 方法 → 工资条格式不允许子类修改
📊 超大总结表格
| 知识点 | 核心内容 |
|---|---|
| 抽象类概念 | 不完整的类,不能描绘具体对象,用 abstract 修饰 |
| 抽象方法 | 只有声明没有实现,abstract 返回值 方法名(); |
| 核心作用 | 编译器强制校验:防止误实例化、防止忘记重写 |
| 不能实例化 | new 抽象类() 编译报错 |
| 不能和 private 搭配 | 子类看不到 private 方法,无法重写 |
| 不能和 final 搭配 | final 禁止重写,abstract 要求重写,矛盾 |
| 不能和 static 搭配 | static 属于类不参与多态,abstract 要求子类重写 |
| 子类必须重写 | 否则子类也要声明为 abstract |
| 可以没有抽象方法 | 作为"标记"禁止实例化 |
| 可以有构造方法 | 给子类用,通过 super() 调用 |
| 可以有普通成员 | 普通方法、属性、静态成员、final 方法都可以 |
| 继承链 | 抽象方法可以传递,直到具体类全部实现 |
| 与多态结合 | 抽象类引用指向子类对象,动态绑定 |
| 内存模型 | 栈中引用 → 堆中子类对象 → 方法区查找实际方法 |
| 模板方法模式 | 抽象类定义算法骨架,子类实现变化步骤 |
| vs 普通父类 | 抽象类多了编译器校验,强制子类实现 |
| vs 接口 | is-a vs has-ability;有构造方法 vs 无;单继承 vs 多实现 |
| 构造方法陷阱 | 不要在构造中调用可重写方法(子类成员未初始化) |
✍️ 写在最后
抽象类是 Java 面向对象的精妙设计之一 – 它不是"类不能写完"的无奈之举,而是一种主动的设计约束。通过把"不该做的事"交给编译器来禁止,抽象类让代码更安全、更规范。
理解抽象类的关键:它不是少了什么,而是多了一层保障。就像红绿灯不是因为路不够宽,而是因为有了规则才更安全。
抽象类和接口是 Java 实现多态的两大支柱。下一篇我们将深入学习接口,看看 Java 是如何通过接口实现更灵活的多态和"多继承"效果的。
如有问题或建议,欢迎留言交流! 🙌
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)