🏛️ 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"关系 – “圆形是一种图形”,“狗是一种动物”

它的设计哲学是:

  1. 提取共性 → 把所有子类共有的属性和行为放到抽象类中
  2. 声明规范 → 把"必须由子类实现"的方法声明为抽象方法
  3. 禁止误用 → 编译器强制检查,不允许实例化抽象类

3️⃣ 抽象类的语法 📝

3.1 定义抽象类

在 Java 中,用 abstract 关键字修饰:

// 抽象类:被 abstract 修饰的类
public abstract class Shape {
    // 抽象方法:被 abstract 修饰的方法,没有方法体(没有大括号)
    abstract public void draw();

    // 抽象类也是类,也可以有普通方法和属性
    public double getArea() {
        return area;
    }

    protected double area;  // 面积
}

🔑 语法要点:

  1. abstract class 类名 → 定义抽象类
  2. abstract 返回值 方法名(); → 定义抽象方法(没有方法体,直接分号结束)
  3. 抽象类中可以有普通方法和属性,甚至构造方法
  4. 抽象方法必须在抽象类中(有抽象方法的类一定是抽象类)
  5. abstract 不能和 finalprivatestatic 同时修饰方法

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,子类根本看不见,还怎么重写?

抽象方法的访问权限必须是子类可见的:publicprotected 或默认(包权限)

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("睡觉...");
    }
    // 没有抽象方法也可以!
}

📌 关键结论:

  • 有抽象方法 → 一定是抽象类 ✅
  • 抽象类 → 不一定有抽象方法 ✅

没有抽象方法的抽象类有什么用?

  1. 禁止实例化 → 作为"标记",表示这个类不应该被直接使用
  2. 为将来扩展预留 → 后续可能需要添加抽象方法
  3. 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 模板方法模式的结构

在这里插入图片描述

🔑 模板方法模式的三要素:

  1. 模板方法:final 修饰,定义算法骨架,子类不能改变流程
  2. 具体方法:通用步骤,子类直接继承使用
  3. 抽象方法:变化步骤,子类必须实现

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 关系(能做什么)
典型代表 AnimalShapeVehicle RunnableComparableCloneable

💡 下一篇我们将深入学习接口,届时会有更详细的对比和应用


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();
}

答: 不能。abstractprivate 不能同时修饰方法。抽象方法的目的是让子类重写,而 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 元 =====

💡 这个示例完美展示了抽象类的核心价值:

  1. 抽象方法 calculateSalary() → 强制每种员工必须实现自己的薪资计算
  2. 具体方法 printPaySlip() → 所有员工共享工资条打印逻辑
  3. 多态 + 抽象类 → 统一遍历处理,无需关心具体类型
  4. 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 是如何通过接口实现更灵活的多态和"多继承"效果的。

如有问题或建议,欢迎留言交流! 🙌

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐