- 0
- 636 words
继承学完了,里氏替换原则也懂了。现在问一个问题:如果父类中有一个方法,每个子类的实现都不一样,那父类写这个方法的代码有什么意义?
比如动物会「叫」——狗汪汪叫、猫喵喵叫、牛哞哞叫。你总不能写一个Animal.eat()的方法体,然后子类全给重写了吧?那父类的方法体就成摆设了,而且还容易犯一个错误——万一哪个子类忘了重写,就直接调了父类的空实现,Bug 静悄悄就来了。
Java 的设计者们当然想到了这个问题。他们提供了一个神奇的语法——抽象方法,它让父类只声明方法签名,不给实现体,强制子类去实现。如果一个类里包含抽象方法,那这个类自己也得声明为抽象类。
抽象类 = 「部分完成」的类,它像一份半成品蓝图,把一些核心细节留给子类去完成。
一、先看感性的理解
我们来类比一个现实场景:你开了一个「厨师培训班」,培训流程是统一的——学员进教室、穿厨师服、老师讲理论、学员实操、品尝点评、下课离场。但是「实操」环节做什么菜呢?川菜班做麻婆豆腐、西点班做蛋糕、日料班做寿司……
你设计的培训流程里,「实操」环节只写了「学员实操并完成菜品」,但具体做啥——每个班不一样。你不可能替所有班的实操写好具体步骤。
在 Java 里,这就叫抽象方法:我知道要有这么个步骤,但具体怎么做,由子类决定。
二、语法入门
2.1 定义一个抽象类
// 抽象类用 abstract 关键字修饰
public abstract class Animal {
// 普通属性
protected String name;
protected int age;
// 普通方法——有方法体
public void eat() {
System.out.println(name + " 正在吃东西……");
}
// 抽象方法——没有方法体,只有声明
public abstract void sound();
// 抽象方法也可以有参数和返回值
public abstract String getInfo();
}
注意几个关键点:
- 使用
abstract关键字修饰类 → 这个类就是抽象类 - 抽象方法也只有
abstract关键字,没有方法体(连花括号都没有,直接分号结尾) - 抽象类可以同时包含普通方法和抽象方法——它不是”完全不可用的类”,而是”部分实现了的类”
- 抽象类也可以有构造器、成员变量、静态方法——普通类能有的它基本都有
2.2 子类实现抽象方法
// 子类必须实现父类所有的抽象方法,否则子类也要声明为抽象类
public class Dog extends Animal {
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void sound() {
System.out.println(name + " 汪汪汪!");
}
@Override
public String getInfo() {
return "狗名:" + name + ",年龄:" + age + "岁";
}
}
public class Cat extends Animal {
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void sound() {
System.out.println(name + " 喵喵喵~");
}
@Override
public String getInfo() {
return "猫名:" + name + ",年龄:" + age + "岁";
}
}
2.3 使用抽象类和子类
public class Main {
public static void main(String[] args) {
// ❌ 抽象类不能直接 new
// Animal animal = new Animal(); // 编译错误!
// ✅ 但可以声明抽象类的引用,指向子类对象(向上转型)
Animal dog = new Dog("旺财", 3);
Animal cat = new Cat("咪咪", 2);
dog.eat(); // 调的是普通方法(继承自Animal)
dog.sound(); // 调的是 Dog 重写的 sound()
System.out.println(dog.getInfo());
cat.eat();
cat.sound();
System.out.println(cat.getInfo());
}
}
输出:
旺财 正在吃东西……
旺财 汪汪汪!
狗名:旺财,年龄:3岁
咪咪 正在吃东西……
咪咪 喵喵喵~
猫名:咪咪,年龄:2岁
这样就完美了——Animal 定义了一个「合约」:所有动物都会叫,但具体怎么叫,每个动物自己决定。如果有人想新增一个 Pig 类,编译器会强制他实现 sound() 和 getInfo() 方法,漏一个都编译不通过。合约强制遵守,这就叫设计保障。
三、抽象类的核心特性与注意事项
3.1 抽象类不能实例化
这是最基础但又最好理解的一条。new Animal() 直接编译报错。为什么?因为你创建一个”动物”对象,它叫不出来,没有意义——抽象类天生就是不完整的。
但反过来,上面你看到了:抽象类可以有构造器。这矛盾吗?不矛盾。 构造器是给子类用的——子类创建对象时,会通过 super() 调用父类的构造器来初始化父类中定义的属性。
public abstract class Animal {
protected String name;
protected int age;
// 抽象类可以有构造器,但只能被子类调用
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public abstract void sound();
}
public class Dog extends Animal {
public Dog(String name, int age) {
super(name, age); // 调用父类构造器
}
@Override
public void sound() {
System.out.println(name + " 汪汪汪!");
}
}
3.2 抽象类可以有成员变量和常量
抽象类可以定义实例变量,也可以定义常量 — 这些都可以被继承给子类使用。
public abstract class Shape {
protected String color; // 成员变量
public static final double PI = 3.1415926535; // 常量
public Shape(String color) {
this.color = color;
}
public abstract double area(); // 抽象方法
public abstract double perimeter();
// 普通方法:所有子类共用
public void displayInfo() {
System.out.println("颜色:" + color);
System.out.println("面积:" + area());
System.out.println("周长:" + perimeter());
}
}
public class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double area() {
return PI * radius * radius; // 使用父类定义的 PI
}
@Override
public double perimeter() {
return 2 * PI * radius;
}
}
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 Main {
public static void main(String[] args) {
Shape circle = new Circle("红色", 5.0);
Shape rect = new Rectangle("蓝色", 4.0, 6.0);
circle.displayInfo(); // 调普通方法
System.out.println("---");
rect.displayInfo(); // 多态:不同子类不同行为
}
}
输出:
颜色:红色
面积:78.5398163375
周长:31.415926535
颜色:蓝色
面积:24.0
周长:20.0
3.3 抽象方法不能用 private、static、final 修饰
这是面试中经常挖坑的地方:
| 修饰符 | 为什么不能和 abstract 一起用? |
|---|---|
private | private 方法子类看不到,无法重写,但抽象方法的目标就是让子类实现,自相矛盾。 |
static | static 方法属于类,不能被子类重写(只能隐藏),抽象方法要求动态绑定,矛盾。 |
final | final 方法禁止子类重写,抽象方法强制子类实现,直接冲突。 |
一句话记:抽象方法天生就是要被重写的,所以和禁止重写的关键字(private/static/final)是死对头。
3.4 抽象类可以有静态方法和 main 方法
你没看错——抽象类也能有自己的 main() 方法,可以直接运行:
public abstract class Demo {
public abstract void doSomething();
public static void main(String[] args) {
System.out.println("抽象类也能运行 main 方法!");
// 但不能在这里创建自己的实例
}
}
虽然你不能 new Demo(),但静态方法跟对象没关系,所以完全合法。这可以用来放一些工厂方法或者工具逻辑。
3.5 子类可以不被实现?
如果一个子类不想实现父类的所有抽象方法,那它自己也得是抽象类:
public abstract class Animal {
public abstract void sound();
public abstract void move();
}
// Dog 只实现了 sound(),没实现 move() —— 它必须也声明为 abstract
public abstract class Dog extends Animal {
@Override
public void sound() {
System.out.println("汪汪汪");
}
// move() 没实现,留给更具体的子类
}
// Husky 终于全部实现了
public class Husky extends Dog {
@Override
public void move() {
System.out.println("哈士奇在满屋子跑……");
}
}
这在大型继承体系里很常见——中间层抽象类负责实现一部分抽象方法,剩下的交给更具体的子类。逐层实现,逐渐具体化。
四、抽象类 vs 普通类:什么时候用抽象类?
| 对比维度 | 抽象类 | 普通类 |
|---|---|---|
| 实例化 | ❌ 不能 new | ✅ 可以 new |
| 抽象方法 | ✅ 可以有 | ❌ 不能有 |
| 成员变量 | ✅ 可以有 | ✅ 可以有 |
| 构造器 | ✅ 可以有 | ✅ 可以有 |
| 普通方法 | ✅ 可以有 | ✅ 可以有 |
| 静态方法/常量 | ✅ 可以有 | ✅ 可以有 |
| 设计目的 | 定义公共属性和行为模板,部分行为不确定 | 完整可用的类 |
什么时候用抽象类?记住这三点就够了:
- 多个子类有公共的属性和行为(成员变量 + 普通方法),可以提取到父类复用
- 部分行为每个子类各不相同,父类无法给出有意义的默认实现
- 想让父类不能被直接实例化,防止开发者误用
简单说:你发现不同类之间「有共同点,但某些行为就是不一样」,就把共同点抽象到父类,把不一样的地方声明成抽象方法。
五、实战案例:员工薪资计算系统
光讲理论不过瘾,咱们写一个实际练习。假设你在给一个公司写薪资系统:
- 全职员工(FullTimeEmployee):月薪固定,每月拿固定工资
- 兼职员工(PartTimeEmployee):按小时计薪,月底结算
- 实习生(Intern):按月领固定津贴,但金额比全职少
所有员工都有名字、工号和薪水计算方法,但计算方式不同——典型的抽象类场景。
5.1 抽象父类 Employee
public abstract class Employee {
protected String name;
protected String id;
public Employee(String name, String id) {
this.name = name;
this.id = id;
}
// 抽象方法:计算月薪——每个员工类型不同
public abstract double calculateMonthlySalary();
// 普通方法:打印工资单(所有员工共用)
public void printPaySlip() {
System.out.println("=== 工资单 ===");
System.out.println("姓名:" + name);
System.out.println("工号:" + id);
System.out.println("本月薪资:¥" + calculateMonthlySalary());
System.out.println("==============");
}
}
5.2 全职员工
public class FullTimeEmployee extends Employee {
private double monthlySalary;
public FullTimeEmployee(String name, String id, double monthlySalary) {
super(name, id);
this.monthlySalary = monthlySalary;
}
@Override
public double calculateMonthlySalary() {
// 全职员工简单的固定月薪
return monthlySalary;
}
}
5.3 兼职员工
public class PartTimeEmployee extends Employee {
private double hourlyRate; // 时薪
private int hoursWorked; // 本月工作小时数
public PartTimeEmployee(String name, String id,
double hourlyRate, int hoursWorked) {
super(name, id);
this.hourlyRate = hourlyRate;
this.hoursWorked = hoursWorked;
}
@Override
public double calculateMonthlySalary() {
return hourlyRate * hoursWorked;
}
}
5.4 实习生
public class Intern extends Employee {
private double stipend; // 实习津贴
public Intern(String name, String id, double stipend) {
super(name, id);
this.stipend = stipend;
}
@Override
public double calculateMonthlySalary() {
// 实习生只拿津贴
return stipend;
}
}
5.5 测试运行
public class PayrollSystem {
public static void main(String[] args) {
// 所有员工用 Employee 引用管理——多态
Employee[] employees = new Employee[] {
new FullTimeEmployee("张三", "FT001", 15000.0),
new PartTimeEmployee("李四", "PT001", 50.0, 80),
new Intern("王五", "IN001", 3000.0)
};
// 统一打印工资单
for (Employee emp : employees) {
emp.printPaySlip();
System.out.println();
}
// 计算公司本月总薪资支出
double total = 0;
for (Employee emp : employees) {
total += emp.calculateMonthlySalary();
}
System.out.println("公司本月薪资总支出:¥" + total);
}
}
输出结果:
=== 工资单 ===
姓名:张三
工号:FT001
本月薪资:¥15000.0
==============
=== 工资单 ===
姓名:李四
工号:PT001
本月薪资:¥4000.0
==============
=== 工资单 ===
姓名:王五
工号:IN001
本月薪资:¥3000.0
==============
公司本月薪资总支出:¥22000.0
看到了吧——新增一种员工类型(比如改成Contractor外包员工),只需要再继承 Employee,实现一个 calculateMonthlySalary() 就行了。原有的循环管理代码一个字都不用改。开闭原则(对扩展开放,对修改关闭),抽象类帮我们做到了。
六、抽象类与继承的对比总结
到目前为止,你已经接触了继承和抽象类。做个对比:
| 对比维度 | 普通继承 | 抽象类 |
|---|---|---|
| 父类是否有方法体 | 所有方法都有实现 | 部分方法没有实现(抽象方法) |
| 父类能否实例化 | ✅ 可以 | ❌ 不可以 |
| 子类必须实现的 | 选择性重写(不强制) | 强制所有抽象方法 |
| 设计意图 | 代码复用 + 层次关系 | 定义行为规范 + 约束子类 |
有继承不一定需要抽象类,但有抽象方法必须是抽象类。当你写父类发现某个方法写不出通用的实现,或者写出来也没意义反正子类都要重写——那就该用抽象方法了。
七、面试中常见问题
整理几个面试高频题,帮你加深理解:
Q1:抽象类和普通类的区别?
最简单的答法:抽象类用 abstract 修饰、不能 new、可以有抽象方法——其他和普通类一样。抽象类的核心价值是定义行为规范,普通类的核心价值是直接使用。
Q2:抽象方法能不能是 private 的?
不能。private 方法子类不可见,而抽象方法需要子类实现——private + abstract = 逻辑死循环。
Q3:抽象类能不能没有抽象方法?
可以!你可以声明一个没有抽象方法的抽象类。虽然少见,但有时候你只是不想让别人 new 这个类,可以用 abstract class 把它卡住。比如 Java 的 HttpServlet 就是一个抽象类,它所有方法都有默认实现,但它不让你 new——必须继承重写。
Q4:抽象类能不能实现接口?
能!而且接口的抽象方法在抽象类里可以选择性实现——不实现的留给子类。这个我们在讲接口的时候会详细聊到。
结语
这篇我们讲了抽象类与抽象方法的核心概念和用法。一句话总结:抽象类是一份「半成品」模板,把确定的东西写好,把不确定的东西留给子类去填充。
但你有没有注意到,薪资系统里有个问题:实习生如果表现好,是不是可以转全职?Java 的类只能单继承——Intern一旦继承了 Employee,就不能再继承其他类了。如果「转全职」需要把 Intern 对象变成 FullTimeEmployee,那就尴尬了。
Java 的设计者用另一种语法解决了这个问题——接口(Interface)。下一篇咱们就聊聊接口,看它跟抽象类有什么区别、什么时候用接口、什么时候用抽象类。
代码写多了你自然会记住一句话:「抽象类是 is-a 关系,接口是 can-do 关系。」——下一篇,咱们来证明这句话。
我们下一篇见 🚀