前言
在学习 Java 的过程中,当你觉得自己已经掌握了"类"和"继承"之后,abstract 这个关键字会突然冒出来,带来一个新概念------抽象类。它和普通类有什么区别?为什么要用它?什么时候该用它?这篇文章从普通类出发,讲清楚抽象类的来龙去脉。
一、概念:什么是抽象类
1.1 从一个问题开始
假设你要设计一个"动物"系统。狗、猫、鸟......它们都有名字、都会吃东西,但"叫"的方式各不相同。你写了一个普通类 Animal:
java
// 动物
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.");
}
}
问题出在哪?
java
Animal animal = new Animal("某动物"); // 能实例化!但"某动物"怎么叫?
animal.makeSound(); // 空实现,什么也不做
// 如果某个子类忘记重写 makeSound(),编译器不会报错
// 只有运行时才发现"叫"没有效果
普通类有两个问题:
- 无法阻止实例化------"动物"是一个抽象概念,不应该被直接创建
- 无法强制子类实现某个方法------子类忘了重写,编译器也不报错
1.2 抽象类的定义
抽象类 就是在这两个问题的基础上诞生的。它在普通类前面加了 abstract 关键字,允许你定义没有方法体的方法(抽象方法),强制子类必须实现。
一句话理解:抽象类是一张"没画完的设计图"------总体框架有了,某些细节留给施工方去完成。
java
// 加 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 逐条说明
java
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 抽象类的子类
java
// 狗------继承 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.");
}
}
java
// 猫------继承 Animal,实现自己的叫声
public class Cat extends Animal {
public Cat(String name) {
super(name); // 调用抽象类的构造方法
}
@Override
public void makeSound() {
System.out.println(getName() + ": 喵喵喵!");
}
}
使用:
java
// 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 抽象类还可以继承抽象类
java
// 顶层抽象类:定义所有动物的共同行为
public abstract class Animal {
public abstract void makeSound();
}
java
// 中间层抽象类:宠物,扩展了"主人"属性,可以不实现父类抽象方法
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();
}
java
// 具体类:狗,必须实现所有继承链上的抽象方法
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 约束------强制子类实现
java
// 没有 abstract:子类忘了重写也不报错,运行时才发现
public class Animal {
public void makeSound() { } // 空实现
}
java
// 有 abstract:子类忘了实现,编译直接报错
public abstract class Animal {
public abstract void makeSound(); // 没有实现,必须由子类提供
}
价值:把运行时错误提前到编译期发现。
3.2 复用------共享公共代码
java
// 抽象类:共享公共属性和方法,子类只需实现差异部分
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 模板方法模式(最核心的场景)
场景描述:多个子类有相同的算法流程,只是某些步骤不同。
java
/**
* 模板方法模式:制作饮料
* 抽象类定义流程骨架(模板),子类实现具体步骤
*/
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()就是一个钩子------咖啡不重写它(默认加调料),茶重写了它(不加调料)。
java
// 咖啡:冲泡咖啡粉,加糖和牛奶
public class Coffee extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("用沸水冲泡咖啡粉");
}
@Override
protected void addCondiments() {
System.out.println("加糖和牛奶");
}
}
java
// 茶:浸泡茶叶,通过钩子方法选择不加调料
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;
}
}
使用:
java
CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();
// 烧开水 → 用沸水冲泡咖啡粉 → 倒入杯中 → 加糖和牛奶
CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
// 烧开水 → 用沸水浸泡茶叶 → 倒入杯中(不加调料)
设计思想 :模板方法用
final锁住算法骨架,用abstract留出可变步骤,用钩子方法提供可选的扩展点。这就是"开闭原则"------对扩展开放(新增子类),对修改关闭(不改模板)。
4.2 骨架实现模式(为接口提供默认实现)
场景描述:接口定义了很多方法,但大多数实现类只需要关心其中几个核心方法。抽象类提供一个"骨架",把不关心的方法给出默认实现。
java
// 接口:定义了 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();
}
java
// 抽象类:提供骨架实现,子类只需实现 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);
}
java
// 子类:只关心"怎么输出",不用管日志级别判断
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);
}
}
java
// 文件日志:将日志写入指定文件
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);
}
}
使用:
java
Logger logger = new ConsoleLogger("APP", 2); // INFO 级别
logger.debug("不会输出"); // 级别不够
logger.info("服务启动"); // [INFO] APP - 服务启动
logger.error("连接超时"); // [ERROR] APP - 连接超时
4.3 通用基类(共享状态和行为)
"状态"就是对象持有的数据,也就是成员变量(字段)。 比如
id、createdAt、updatedAt就是状态------多个方法都要用它们,它们是对象内部共享的数据。
场景描述:多个类有相同的属性和方法,提取到抽象基类中。
java
// 通用实体基类:封装所有实体的公共字段(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 ...
}
java
// 用户实体:继承 BaseEntity,自动拥有 id、时间戳等公共字段
public class User extends BaseEntity {
private String username;
private String email;
}
java
// 订单实体:继承 BaseEntity,自动拥有 id、时间戳等公共字段
public class Order extends BaseEntity {
private BigDecimal amount; // BigDecimal 是 Java 中表示精确小数的类,比 double 更适合表示金额
private String status;
}
五、举例:完整的代码示例
5.1 图形面积计算
java
// 图形抽象类:定义面积和周长的计算契约,所有图形共享颜色属性
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()));
}
}
java
// 圆形:用半径计算面积(π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;
}
}
java
// 矩形:用长宽计算面积(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);
}
}
java
// 三角形:用海伦公式计算面积,三条边之和为周长
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;
}
}
使用:
java
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 员工工资计算
java
// 员工抽象类:不同类型员工的工资计算方式不同,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; }
}
java
// 全职员工:固定月薪,工资 = 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;
}
}
java
// 兼职员工:按小时计薪,工资 = 时薪 × 工时
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;
}
}
java
// 提成员工:底薪 + 销售额 × 提成比例
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;
}
}
使用:
java
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 只有抽象方法、没有状态的抽象类
java
// 反例:抽象类里只有抽象方法,没有任何成员变量和具体实现
// 这意味着抽象类的"复用"和"模板"两大作用都没用上
public abstract class Flyable {
public abstract void fly();
}
java
// 正确做法:加上共享状态或具体方法,发挥抽象类的价值
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 继承层次过深
java
// 反例: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 { }
java
// 正确做法:扁平化继承层次,或用组合代替继承
public abstract class Vehicle { }
public class TeslaModelY extends Vehicle { }
判断标准:继承层次超过 3 层就要警惕。考虑用组合(has-a)代替继承(is-a)。
6.3 抽象类承担过多职责
java
// 反例:上帝类,什么都往里塞
public abstract class BaseEntity {
public abstract void validate();
public abstract void save();
public abstract void sendNotification();
public String toJson() { return "{}"; }
}
java
// 正确做法:单一职责,抽象类只做一件事
public abstract class BaseEntity {
private Long id;
public abstract void validate(); // 只负责验证
// save、toJson 等职责交给其他类
}
6.4 为了用抽象类而用抽象类
java
// 反例:只有一个子类,没有必要用抽象类
public abstract class Animal {
public abstract void makeSound();
}
java
public class Dog extends Animal {
@Override
public void makeSound() { System.out.println("汪"); }
}
// 只有一个 Dog,没有 Cat、Bird......抽象类毫无意义
// 正确做法:至少有两个子类时,才考虑提取抽象类
七、速查清单
| 问题 | 答案 |
|---|---|
| 抽象类用什么修饰? | abstract class |
| 抽象类能实例化吗? | 不能 |
| 抽象类能有构造方法吗? | 能,给子类通过 super() 调用 |
| 抽象类能有成员变量吗? | 能,和普通类一样 |
| 抽象类能有普通方法吗? | 能,和普通类一样 |
| 抽象类能有静态方法吗? | 能,和普通类一样 |
| 一个抽象方法都没有的类可以是抽象类吗? | 可以 |
| 子类必须实现所有抽象方法吗? | 是,否则子类也要声明为 abstract |
| 抽象类能实现接口吗? | 能 |
| 抽象类能继承抽象类吗? | 能 |
| 什么时候用抽象类? | 多个子类有相同属性/行为 + 需要强制子类实现某些方法 |
| 典型设计模式? | 模板方法模式 |
八、面试口述:什么是抽象类
抽象类就是用 abstract 关键字修饰的类,它和普通类几乎一样,能拥有构造方法、成员变量、普通方法和静态方法,唯一的区别是:抽象类可以定义没有方法体的抽象方法,并且自身不能被实例化。子类继承抽象类后,必须实现所有抽象方法,否则子类自己也要声明为抽象类。这样就把运行时才能发现的问题提前到了编译期。
抽象类有三个核心作用:约束、复用、模板。约束是指用抽象方法强制子类必须实现某些行为;复用是指把公共的属性和方法放在抽象类中,避免子类重复编写;模板是指用 final 方法定义算法骨架,把可变的步骤声明为抽象方法让子类实现------这就是模板方法模式,也是抽象类最典型的应用场景。
需要注意的是,抽象类的构造方法虽然不能直接用来 new 对象,但子类会通过 super() 调用它,所以常用来做初始化和参数校验。另外,即使一个抽象方法都没有,类也可以声明为 abstract,纯粹用来禁止实例化。