前言

在学习 Java 的过程中,当你觉得自己已经掌握了"类"和"继承"之后,abstract 这个关键字会突然冒出来,带来一个新概念——抽象类。它和普通类有什么区别?为什么要用它?什么时候该用它?这篇文章从普通类出发,讲清楚抽象类的来龙去脉。


一、概念:什么是抽象类

1.1 从一个问题开始

假设你要设计一个"动物"系统。狗、猫、鸟……它们都有名字、都会吃东西,但"叫"的方式各不相同。你写了一个普通类 Animal

// 动物
public class Animal {

    private String name;

    // 构造方法
    public Animal(String name) {
        this.name = name;
    }

    // 问题:动物都会叫,但怎么叫?
    // 狗是"汪汪",猫是"喵喵",鱼……不出声
    // 在 Animal 这个层面,你没法给出一个通用的实现
    public void makeSound() {
        // 空实现?——子类忘记重写也不会报错
        // 抛异常?——运行时才发现问题
    }

    // 普通方法
    public String getName() {
        return name;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }
}

问题出在哪?

Animal animal = new Animal("某动物"); // 能实例化!但"某动物"怎么叫?
animal.makeSound();                    // 空实现,什么也不做

// 如果某个子类忘记重写 makeSound(),编译器不会报错
// 只有运行时才发现"叫"没有效果

普通类有两个问题:

  1. 无法阻止实例化——"动物"是一个抽象概念,不应该被直接创建
  2. 无法强制子类实现某个方法——子类忘了重写,编译器也不报错

1.2 抽象类的定义

抽象类就是在这两个问题的基础上诞生的。它在普通类前面加了 abstract 关键字,允许你定义没有方法体的方法(抽象方法),强制子类必须实现。

一句话理解:抽象类是一张"没画完的设计图"——总体框架有了,某些细节留给施工方去完成。

// 加 abstract 关键字 → 变成抽象类
public abstract class Animal {

    private String name;

    // 构造方法:和普通类一样
    public Animal(String name) {
        this.name = name;
    }

    // 加 abstract 关键字 → 抽象方法(没有方法体)
    // 子类必须实现,否则编译报错!
    public abstract void makeSound();

    // 普通方法:和普通类一样,有方法体
    public String getName() {
        return name;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }
}

和普通类相比,只变了三点:

变化 普通类 抽象类
类声明 class Animal abstract class Animal(加了 abstract
方法 全部必须有方法体 可以有没有方法体的抽象方法
实例化 可以 new 不能 new

除此之外,抽象类能做的,普通类都能做。


二、性质:抽象类的完整特性

2.1 逐条说明

public abstract class Animal {

    // 1. 成员变量:和普通类完全一样,任意类型和访问修饰符
    private String name;                // 私有变量
    protected int age;                  // 受保护变量
    public static final int MAX_AGE = 200; // 常量
    private static int count = 0;       // 静态变量

    // 2. 构造方法:和普通类完全一样,支持重载
    //    虽然不能直接 new,但子类会通过 super() 调用它
    public Animal(String name) {
        this.name = name;
        count++;
    }

    public Animal(String name, int age) {
        this(name);
        this.age = age;
    }

    // 3. 抽象方法:普通类不能有,抽象类才能有
    //    没有方法体,子类必须实现(除非子类也是抽象类)
    public abstract void makeSound();

    // 4. 普通方法:和普通类完全一样
    public void eat() {
        System.out.println(name + " is eating.");
    }

    // 5. 静态方法:和普通类完全一样
    public static int getCount() {
        return count;
    }

    // 6. 可以继承另一个类(单继承)
    // 7. 可以实现接口(多实现)
    // 8. 可以有 final 方法(禁止子类重写)
    public final String getDescription() {
        return "This is " + name;
    }
}

2.2 特性对照表

性质 普通类 抽象类 变化说明
能否被实例化 不能 new Animal() 编译报错
能否有构造方法 不变——给子类通过 super() 调用
能否有成员变量 不变——任意类型和访问修饰符
能否有抽象方法 不能 新增能力——强制子类实现
能否有普通方法 不变
能否有静态方法 不变
能否有 final 方法 不变
继承父类 单继承 单继承 不变
实现接口 多实现 多实现 不变

可以看出,抽象类 = 普通类 + abstract 关键字 + 抽象方法 - 实例化能力。它是对普通类的小幅增强,而不是一个全新的概念。

2.3 抽象类的子类

// 狗——继承 Animal,实现自己的叫声
public class Dog extends Animal {

    public Dog(String name) {
        super(name); // 调用抽象类的构造方法
    }

    @Override  // 表示"我正在重写父类的方法"——如果方法签名写错了,编译器会报错
    public void makeSound() {
        System.out.println(getName() + ": 汪汪汪!");
    }

    // eat() 继承自 Animal,可以直接使用,也可以选择重写
    @Override
    public void eat() {
        System.out.println(getName() + " is eating dog food.");
    }
}
// 猫——继承 Animal,实现自己的叫声
public class Cat extends Animal {

    public Cat(String name) {
        super(name); // 调用抽象类的构造方法
    }

    @Override
    public void makeSound() {
        System.out.println(getName() + ": 喵喵喵!");
    }
}

使用:

// Animal a = new Animal("xx");  // 编译错误!抽象类不能实例化
// 这正是我们想要的——"动物"是一个抽象概念,不应该被直接创建

Animal dog = new Dog("旺财");    // 多态:父类引用指向子类对象
dog.makeSound();                  // 旺财: 汪汪汪!
dog.eat();                        // 旺财 is eating dog food.

Animal cat = new Cat("咪咪");
cat.makeSound();                  // 咪咪: 喵喵喵!
cat.eat();                        // 咪咪 is eating.(未重写,用父类的)

// 如果某个子类忘记实现 makeSound(),编译器直接报错:
// "Dog is not abstract and does not override abstract method makeSound() in Animal"

什么是"多态"? “多态"就是"同一个指令,不同的行为”。上面 dog.makeSound() 输出"汪汪汪",cat.makeSound() 输出"喵喵喵"——同样是 makeSound(),不同对象有不同的表现。Java 通过"父类引用指向子类对象"(如 Animal dog = new Dog(...))来实现多态,调用方法时会自动执行子类重写的版本。

2.4 抽象类还可以继承抽象类

// 顶层抽象类:定义所有动物的共同行为
public abstract class Animal {
    public abstract void makeSound();
}
// 中间层抽象类:宠物,扩展了"主人"属性,可以不实现父类抽象方法
public abstract class Pet extends Animal {
    private String owner;

    public Pet(String owner) {
        this.owner = owner;
    }

    public String getOwner() {
        return owner;
    }

    // Pet 也可以不实现 makeSound(),继续留给下一层子类
    public abstract String getPetType();
}
// 具体类:狗,必须实现所有继承链上的抽象方法
public class Dog extends Pet {

    public Dog(String owner) {
        super(owner);
    }

    @Override
    public void makeSound() {
        System.out.println("汪汪汪!");
    }

    @Override
    public String getPetType() {
        return "Dog";
    }
}

三、作用:为什么要用抽象类

抽象类的作用可以概括为三个词:约束、复用、模板

3.1 约束——强制子类实现

// 没有 abstract:子类忘了重写也不报错,运行时才发现
public class Animal {
    public void makeSound() { }  // 空实现
}
// 有 abstract:子类忘了实现,编译直接报错
public abstract class Animal {
    public abstract void makeSound();  // 没有实现,必须由子类提供
}

价值:把运行时错误提前到编译期发现。

3.2 复用——共享公共代码

// 抽象类:共享公共属性和方法,子类只需实现差异部分
public abstract class Animal {

    private String name;

    public Animal(String name) { this.name = name; }

    // 所有子类共享这份代码,不用每个子类都写一遍
    public String getName() {
        return name;
    }

    public void eat() {
        System.out.println(name + " is eating.");
    }

    public void sleep() {
        System.out.println(name + " is sleeping.");
    }

    public abstract void makeSound();
}

3.3 模板——定义算法骨架

这是抽象类最核心的价值,详见下一节"经典场景"。

3.4 三大作用总结

作用 核心思想 解决的问题 关键字/机制
约束 强制子类实现 子类忘记重写,运行时才发现 abstract 方法
复用 共享公共代码 多个子类重复编写相同逻辑 普通方法 + 成员变量
模板 固定算法骨架,留出可变步骤 多个子类算法流程相同,仅步骤不同 final 模板方法 + abstract 步骤

三者的关系:

约束(abstract 方法)
  ↓ 强制子类实现
复用(普通方法 + 成员变量)
  ↓ 子类共享公共代码
模板(final 方法调用 abstract 方法)
  ↓ 固定流程,变化点留给子类

一句话总结:抽象类用 abstract 方法约束子类必须实现什么,用普通方法和成员变量复用公共代码,用模板方法模式把约束和复用组合成固定的算法骨架——这就是抽象类的全部价值。


四、场景:抽象类的典型应用

4.1 模板方法模式(最核心的场景)

场景描述:多个子类有相同的算法流程,只是某些步骤不同。

/**
 * 模板方法模式:制作饮料
 * 抽象类定义流程骨架(模板),子类实现具体步骤
 */
public abstract class CaffeineBeverage {

    /**
     * 模板方法:定义为 final,防止子类重写算法骨架
     * 流程固定:烧水 → 冲泡 → 倒杯 → 加调料
     * 其中"冲泡"和"加调料"留给子类实现
     */
    public final void prepareRecipe() {
        boilWater(); // 烧水
        brew(); // 冲泡
        pourInCup(); // 倒杯
        if (customerWantsCondiments()) {
            addCondiments(); // 加调料
        }
    }

    // 公共实现:所有子类共享
    private void boilWater() {
        System.out.println("烧开水");
    }

    private void pourInCup() {
        System.out.println("倒入杯中");
    }

    // 抽象方法:子类必须实现
    protected abstract void brew();
    protected abstract void addCondiments();

    // 钩子方法:子类可以选择重写,也可以使用默认实现
    protected boolean customerWantsCondiments() {
        return true;
    }
}

什么是"钩子方法"?

“钩子"就是预留的一个"挂钩”——默认什么都不做(或返回 true),子类如果有特殊需求就"挂"上去(重写),没有就用默认的。

它和抽象方法的区别:抽象方法必须重写,钩子方法可以选择重写。 上面的 customerWantsCondiments() 就是一个钩子——咖啡不重写它(默认加调料),茶重写了它(不加调料)。

// 咖啡:冲泡咖啡粉,加糖和牛奶
public class Coffee extends CaffeineBeverage {

    @Override
    protected void brew() {
        System.out.println("用沸水冲泡咖啡粉");
    }

    @Override
    protected void addCondiments() {
        System.out.println("加糖和牛奶");
    }
}
// 茶:浸泡茶叶,通过钩子方法选择不加调料
public class Tea extends CaffeineBeverage {

    @Override
    protected void brew() {
        System.out.println("用沸水浸泡茶叶");
    }

    @Override
    protected void addCondiments() {
        System.out.println("加柠檬");
    }

    // 重写钩子方法
    @Override
    protected boolean customerWantsCondiments() {
        return false;
    }
}

使用:

CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();
// 烧开水 → 用沸水冲泡咖啡粉 → 倒入杯中 → 加糖和牛奶

CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
// 烧开水 → 用沸水浸泡茶叶 → 倒入杯中(不加调料)

设计思想:模板方法用 final 锁住算法骨架,用 abstract 留出可变步骤,用钩子方法提供可选的扩展点。这就是"开闭原则"——对扩展开放(新增子类),对修改关闭(不改模板)。

4.2 骨架实现模式(为接口提供默认实现)

场景描述:接口定义了很多方法,但大多数实现类只需要关心其中几个核心方法。抽象类提供一个"骨架",把不关心的方法给出默认实现。

// 接口:定义了 8 个方法
public interface Logger {
    void trace(String msg);
    void debug(String msg);
    void info(String msg);
    void warn(String msg);
    void error(String msg);
    void fatal(String msg);
    String getName();
    void close();
}
// 抽象类:提供骨架实现,子类只需实现 1 个核心方法 doLog()
public abstract class AbstractLogger implements Logger {

    protected String name;
    protected int level; // 0=TRACE, 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR, 5=FATAL

    public AbstractLogger(String name, int level) {
        this.name = name;
        this.level = level;
    }

    @Override
    public String getName() {
        return name;
    }

    // 所有日志级别都委托给同一个 doLog() 方法
    @Override
    public void trace(String msg) { if (level <= 0) doLog("TRACE", msg); }

    @Override
    public void debug(String msg) { if (level <= 1) doLog("DEBUG", msg); }

    @Override
    public void info(String msg)  { if (level <= 2) doLog("INFO", msg); }

    @Override
    public void warn(String msg)  { if (level <= 3) doLog("WARN", msg); }

    @Override
    public void error(String msg) { if (level <= 4) doLog("ERROR", msg); }

    @Override
    public void fatal(String msg) { if (level <= 5) doLog("FATAL", msg); }

    @Override
    public void close() { /* 默认什么都不做 */ }

    // 子类只需实现这个核心方法
    protected abstract void doLog(String level, String msg);
}
// 子类:只关心"怎么输出",不用管日志级别判断
public class ConsoleLogger extends AbstractLogger {

    public ConsoleLogger(String name, int level) {
        super(name, level);
    }

    @Override
    protected void doLog(String level, String msg) {
        System.out.println("[" + level + "] " + name + " - " + msg);
    }
}
// 文件日志:将日志写入指定文件
public class FileLogger extends AbstractLogger {

    private String filePath;

    public FileLogger(String name, int level, String filePath) {
        super(name, level);
        this.filePath = filePath;
    }

    @Override
    protected void doLog(String level, String msg) {
        System.out.println("写入 " + filePath + ": [" + level + "] " + msg);
    }
}

使用:

Logger logger = new ConsoleLogger("APP", 2); // INFO 级别
logger.debug("不会输出");                      // 级别不够
logger.info("服务启动");                       // [INFO] APP - 服务启动
logger.error("连接超时");                      // [ERROR] APP - 连接超时

4.3 通用基类(共享状态和行为)

"状态"就是对象持有的数据,也就是成员变量(字段)。 比如 idcreatedAtupdatedAt就是状态——多个方法都要用它们,它们是对象内部共享的数据。

场景描述:多个类有相同的属性和方法,提取到抽象基类中。

// 通用实体基类:封装所有实体的公共字段(id、创建时间、更新时间)和通用行为
public abstract class BaseEntity {

    private Long id;
    private LocalDateTime createdAt;   // LocalDateTime 是 Java 8+ 提供的日期时间类,now() 获取当前时间
    private LocalDateTime updatedAt;

    // 公共逻辑
    public boolean isNew() {
        return id == null;
    }

    public void prePersist() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    public void preUpdate() {
        this.updatedAt = LocalDateTime.now();
    }

    // getter/setter ...
}
// 用户实体:继承 BaseEntity,自动拥有 id、时间戳等公共字段
public class User extends BaseEntity {
    private String username;
    private String email;
}
// 订单实体:继承 BaseEntity,自动拥有 id、时间戳等公共字段
public class Order extends BaseEntity {
    private BigDecimal amount;  // BigDecimal 是 Java 中表示精确小数的类,比 double 更适合表示金额
    private String status;
}

五、举例:完整的代码示例

5.1 图形面积计算

// 图形抽象类:定义面积和周长的计算契约,所有图形共享颜色属性
public abstract class Shape {

    private String color;

    public Shape(String color) {
        this.color = color;
    }

    // 抽象方法:每种图形的面积计算方式不同
    public abstract double area();

    // 抽象方法:每种图形的周长计算方式不同
    public abstract double perimeter();

    // 普通方法:所有图形共享
    public String getColor() {
        return color;
    }

    // 普通方法:基于抽象方法 area() 的组合逻辑
    public void printInfo() {
        System.out.println("颜色:" + color
                + ",面积:" + String.format("%.2f", area())
                + ",周长:" + String.format("%.2f", perimeter()));
    }
}
// 圆形:用半径计算面积(πr²)和周长(2πr)
public class Circle extends Shape {

    private double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }
}
// 矩形:用长宽计算面积(w×h)和周长(2(w+h))
public class Rectangle extends Shape {

    private double width;
    private double height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }

    @Override
    public double perimeter() {
        return 2 * (width + height);
    }
}
// 三角形:用海伦公式计算面积,三条边之和为周长
public class Triangle extends Shape {

    private double a, b, c;

    public Triangle(String color, double a, double b, double c) {
        super(color);
        this.a = a;
        this.b = b;
        this.c = c;
    }

    @Override
    public double area() {
        double s = (a + b + c) / 2;
        return Math.sqrt(s * (s - a) * (s - b) * (s - c));
    }

    @Override
    public double perimeter() {
        return a + b + c;
    }
}

使用:

Shape[] shapes = {
    new Circle("红色", 5),
    new Rectangle("蓝色", 4, 6),
    new Triangle("绿色", 3, 4, 5)
};

for (Shape shape : shapes) {
    shape.printInfo();
}
// 颜色:红色,面积:78.54,周长:31.42
// 颜色:蓝色,面积:24.00,周长:20.00
// 颜色:绿色,面积:6.00,周长:12.00

5.2 员工工资计算

// 员工抽象类:不同类型员工的工资计算方式不同,getDetails() 复用抽象方法
public abstract class Employee {

    private String name;
    private int id;

    public Employee(String name, int id) {
        this.name = name;
        this.id = id;
    }

    // 抽象方法:不同类型员工的工资计算方式不同
    public abstract double calculateSalary();

    // 普通方法
    public String getDetails() {
        return "ID: " + id + ", 姓名: " + name
                + ", 工资: " + String.format("%.2f", calculateSalary());
    }

    public String getName() { return name; }
    public int getId() { return id; }
}
// 全职员工:固定月薪,工资 = monthlySalary
public class FullTimeEmployee extends Employee {

    private double monthlySalary;

    public FullTimeEmployee(String name, int id, double monthlySalary) {
        super(name, id);
        this.monthlySalary = monthlySalary;
    }

    @Override
    public double calculateSalary() {
        return monthlySalary;
    }
}
// 兼职员工:按小时计薪,工资 = 时薪 × 工时
public class PartTimeEmployee extends Employee {

    private double hourlyRate;
    private int hoursWorked;

    public PartTimeEmployee(String name, int id, double hourlyRate, int hoursWorked) {
        super(name, id);
        this.hourlyRate = hourlyRate;
        this.hoursWorked = hoursWorked;
    }

    @Override
    public double calculateSalary() {
        return hourlyRate * hoursWorked;
    }
}
// 提成员工:底薪 + 销售额 × 提成比例
public class CommissionEmployee extends Employee {

    private double baseSalary;
    private double salesAmount;
    private double commissionRate;

    public CommissionEmployee(String name, int id, double baseSalary,
                               double salesAmount, double commissionRate) {
        super(name, id);
        this.baseSalary = baseSalary;
        this.salesAmount = salesAmount;
        this.commissionRate = commissionRate;
    }

    @Override
    public double calculateSalary() {
        return baseSalary + salesAmount * commissionRate;
    }
}

使用:

List<Employee> employees = List.of(
    new FullTimeEmployee("张三", 1, 15000),
    new PartTimeEmployee("李四", 2, 50, 80),
    new CommissionEmployee("王五", 3, 5000, 100000, 0.05)
);

for (Employee e : employees) {
    System.out.println(e.getDetails());
}
// ID: 1, 姓名: 张三, 工资: 15000.00
// ID: 2, 姓名: 李四, 工资: 4000.00
// ID: 3, 姓名: 王五, 工资: 10000.00

六、反例:抽象类的常见误用

6.1 只有抽象方法、没有状态的抽象类

// 反例:抽象类里只有抽象方法,没有任何成员变量和具体实现
// 这意味着抽象类的"复用"和"模板"两大作用都没用上
public abstract class Flyable {
    public abstract void fly();
}
// 正确做法:加上共享状态或具体方法,发挥抽象类的价值
public abstract class Flyable {

    private int altitude; // 共享状态:当前飞行高度

    public Flyable(int altitude) {
        this.altitude = altitude;
    }

    public abstract void fly();

    // 具体方法:复用逻辑
    public void checkAltitude() {
        if (altitude > 10000) {
            System.out.println("高度过高,请注意安全");
        }
    }
}

判断标准:如果一个抽象类里没有成员变量、没有具体方法,说明"复用"和"模板"的价值都没有发挥出来,需要重新审视是否有必要使用抽象类,可能使用接口更规范。

6.2 继承层次过深

// 反例:5 层继承,越来越难维护
public abstract class Vehicle { }
public abstract class MotorVehicle extends Vehicle { }
public abstract class FourWheelVehicle extends MotorVehicle { }
public abstract class Car extends FourWheelVehicle { }
public abstract class SUV extends Car { }
public class TeslaModelY extends SUV { }
// 正确做法:扁平化继承层次,或用组合代替继承
public abstract class Vehicle { }
public class TeslaModelY extends Vehicle { }

判断标准:继承层次超过 3 层就要警惕。考虑用组合(has-a)代替继承(is-a)。

6.3 抽象类承担过多职责

// 反例:上帝类,什么都往里塞
public abstract class BaseEntity {
    public abstract void validate();
    public abstract void save();
    public abstract void sendNotification();
    public String toJson() { return "{}"; }
}
// 正确做法:单一职责,抽象类只做一件事
public abstract class BaseEntity {
    private Long id;
    public abstract void validate();  // 只负责验证
    // save、toJson 等职责交给其他类
}

6.4 为了用抽象类而用抽象类

// 反例:只有一个子类,没有必要用抽象类
public abstract class Animal {
    public abstract void makeSound();
}
public class Dog extends Animal {
    @Override
    public void makeSound() { System.out.println("汪"); }
}
// 只有一个 Dog,没有 Cat、Bird……抽象类毫无意义

// 正确做法:至少有两个子类时,才考虑提取抽象类

七、速查清单

问题 答案
抽象类用什么修饰? abstract class
抽象类能实例化吗? 不能
抽象类能有构造方法吗? 能,给子类通过 super() 调用
抽象类能有成员变量吗? 能,和普通类一样
抽象类能有普通方法吗? 能,和普通类一样
抽象类能有静态方法吗? 能,和普通类一样
一个抽象方法都没有的类可以是抽象类吗? 可以
子类必须实现所有抽象方法吗? 是,否则子类也要声明为 abstract
抽象类能实现接口吗?
抽象类能继承抽象类吗?
什么时候用抽象类? 多个子类有相同属性/行为 + 需要强制子类实现某些方法
典型设计模式? 模板方法模式

八、面试口述:什么是抽象类

抽象类就是用 abstract 关键字修饰的类,它和普通类几乎一样,能拥有构造方法、成员变量、普通方法和静态方法,唯一的区别是:抽象类可以定义没有方法体的抽象方法,并且自身不能被实例化。子类继承抽象类后,必须实现所有抽象方法,否则子类自己也要声明为抽象类。这样就把运行时才能发现的问题提前到了编译期。

抽象类有三个核心作用:约束、复用、模板。约束是指用抽象方法强制子类必须实现某些行为;复用是指把公共的属性和方法放在抽象类中,避免子类重复编写;模板是指用 final 方法定义算法骨架,把可变的步骤声明为抽象方法让子类实现——这就是模板方法模式,也是抽象类最典型的应用场景。

需要注意的是,抽象类的构造方法虽然不能直接用来 new 对象,但子类会通过 super() 调用它,所以常用来做初始化和参数校验。另外,即使一个抽象方法都没有,类也可以声明为 abstract,纯粹用来禁止实例化。

更多推荐