Java 抽象类和接口的区别:从设计场景理解
核心一句话:
抽象类解决"我是谁"的问题,接口解决"我能做什么"的问题。
1. 为什么 Java 要设计抽象类和接口?
在 Java 开发中,经常会遇到这种情况:
- 很多类有相同的属性和方法;
- 很多类虽然不是同一种东西,但具备相同的能力;
- 我们希望代码更清晰、更容易扩展、更少重复。
所以 Java 提供了两种抽象机制:
| 机制 | 主要解决的问题 |
|---|---|
| 抽象类 | 抽取同一类事物的共同特征 |
| 接口 | 抽取不同事物的共同能力 |
2. 抽象类解决什么场景问题?
2.1 适用场景
抽象类适合处理这种关系:
A 是一种 B
例如:
- 狗是一种动物;
- 猫是一种动物;
- 老师是一种人;
- 学生是一种人;
- 微信支付是一种支付方式;
- 支付宝支付也是一种支付方式。
这些对象属于同一大类,有共同属性,也有共同方法。
3. 不使用抽象类会有什么问题?
假设我们要写 Dog 和 Cat 两个类。
java
class Dog {
String name;
int age;
public void sleep() {
System.out.println("动物睡觉");
}
public void eat() {
System.out.println("狗吃骨头");
}
}
class Cat {
String name;
int age;
public void sleep() {
System.out.println("动物睡觉");
}
public void eat() {
System.out.println("猫吃鱼");
}
}
可以发现,Dog 和 Cat 中有重复代码:
java
String name;
int age;
public void sleep() {
System.out.println("动物睡觉");
}
如果动物越来越多,比如 Bird、Tiger、Rabbit,重复代码会越来越多。
这时就可以使用抽象类。
4. 使用抽象类优化代码
4.1 定义抽象父类
java
abstract class Animal {
String name;
int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void sleep() {
System.out.println(name + " 正在睡觉");
}
public abstract void eat();
}
这里有两个重点:
java
public void sleep() {
System.out.println(name + " 正在睡觉");
}
这是普通方法,所有动物都可以复用。
java
public abstract void eat();
这是抽象方法。因为不同动物吃的东西不同,所以父类只规定"动物必须会吃",但不写具体实现。
4.2 子类继承抽象类
java
class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + " 吃骨头");
}
}
java
class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + " 吃鱼");
}
}
4.3 测试代码
java
public class Test {
public static void main(String[] args) {
Animal dog = new Dog("旺财", 3);
Animal cat = new Cat("小花", 2);
dog.sleep();
dog.eat();
cat.sleep();
cat.eat();
}
}
输出结果:
text
旺财 正在睡觉
旺财 吃骨头
小花 正在睡觉
小花 吃鱼
5. 抽象类到底解决了什么?
抽象类主要解决三个问题:
5.1 解决代码重复问题
公共属性和公共方法可以放到父类中。
java
String name;
int age;
public void sleep() {}
这些内容不需要在每个子类里重复写。
5.2 解决统一规范问题
抽象方法可以强制子类实现。
java
public abstract void eat();
只要继承了 Animal,就必须实现 eat() 方法。
5.3 解决多态调用问题
可以用父类类型接收子类对象。
java
Animal animal = new Dog("旺财", 3);
animal.eat();
虽然变量类型是 Animal,但真正执行的是 Dog 中的 eat() 方法。
6. 接口解决什么场景问题?
6.1 适用场景
接口适合处理这种关系:
A 具备某种能力
例如:
- 鸟可以飞;
- 飞机可以飞;
- 超人可以飞;
- 汽车可以被驾驶;
- 人也可以驾驶;
- 打印机可以打印;
- 订单也可以打印。
这些对象不一定属于同一类,但它们具备相同的能力。
7. 为什么这种场景不能只用抽象类?
假设我们想描述"会飞"的东西。
有这些对象:
- 鸟;
- 飞机;
- 超人。
它们都能飞,但它们不是同一种东西:
text
鸟 是 动物
飞机 是 交通工具
超人 是 人
如果强行设计一个父类:
java
abstract class FlyingObject {
public abstract void fly();
}
然后让 Bird、Airplane、Superman 都继承它,看似可以,但设计不合理。
因为飞机不是动物,鸟也不是交通工具,它们只是都具备"飞行能力"。
所以更适合用接口。
8. 使用接口描述能力
8.1 定义接口
java
interface Flyable {
void fly();
}
这个接口表示:
只要实现了
Flyable,就说明这个类具备飞行能力。
8.2 不同类实现接口
java
class Bird implements Flyable {
@Override
public void fly() {
System.out.println("鸟扇动翅膀飞行");
}
}
java
class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("飞机依靠发动机飞行");
}
}
java
class Superman implements Flyable {
@Override
public void fly() {
System.out.println("超人直接飞行");
}
}
8.3 测试代码
java
public class Test {
public static void main(String[] args) {
Flyable bird = new Bird();
Flyable airplane = new Airplane();
Flyable superman = new Superman();
bird.fly();
airplane.fly();
superman.fly();
}
}
输出结果:
text
鸟扇动翅膀飞行
飞机依靠发动机飞行
超人直接飞行
9. 接口到底解决了什么?
接口主要解决三个问题:
9.1 解决能力抽象问题
接口不关心对象是什么,只关心对象能做什么。
java
interface Flyable {
void fly();
}
这表示所有实现类都具备飞行能力。
9.2 解决多实现问题
Java 中一个类只能继承一个父类,但可以实现多个接口。
例如,一个人既可以开车,也可以游泳。
java
interface Drivable {
void drive();
}
interface Swimmable {
void swim();
}
java
class Person implements Drivable, Swimmable {
@Override
public void drive() {
System.out.println("人可以开车");
}
@Override
public void swim() {
System.out.println("人可以游泳");
}
}
如果用抽象类,Java 不允许这样写:
java
// 错误写法:Java 不支持类的多继承
class Person extends Driver, Swimmer {
}
但是接口可以:
java
class Person implements Drivable, Swimmable {
}
9.3 解决程序扩展问题
假设我们写一个方法,让所有能飞的对象都飞起来:
java
public static void makeItFly(Flyable obj) {
obj.fly();
}
调用时:
java
makeItFly(new Bird());
makeItFly(new Airplane());
makeItFly(new Superman());
以后如果新增一个 Drone 无人机类,只需要实现 Flyable 接口:
java
class Drone implements Flyable {
@Override
public void fly() {
System.out.println("无人机通过螺旋桨飞行");
}
}
原来的方法不用修改:
java
makeItFly(new Drone());
这就是接口带来的扩展性。
10. 抽象类和接口可以一起使用
在实际开发中,抽象类和接口经常一起使用。
例如:老师是一种人,同时老师也可以开车。
10.1 定义抽象类 Person
java
abstract class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void sleep() {
System.out.println(name + " 需要睡觉");
}
public abstract void work();
}
10.2 定义接口 Drivable
java
interface Drivable {
void drive();
}
10.3 Teacher 继承抽象类,同时实现接口
java
class Teacher extends Person implements Drivable {
public Teacher(String name, int age) {
super(name, age);
}
@Override
public void work() {
System.out.println(name + " 正在教学");
}
@Override
public void drive() {
System.out.println(name + " 开车去学校");
}
}
10.4 测试代码
java
public class Test {
public static void main(String[] args) {
Teacher teacher = new Teacher("张老师", 35);
teacher.sleep();
teacher.work();
teacher.drive();
}
}
输出结果:
text
张老师 需要睡觉
张老师 正在教学
张老师 开车去学校
这个例子中:
java
class Teacher extends Person
表示:
老师是一种人。
java
class Teacher implements Drivable
表示:
老师具备开车能力。
11. 真实开发场景:支付系统
假设我们在开发一个订单支付系统,需要支持:
- 微信支付;
- 支付宝支付;
- 银行卡支付。
不同支付方式的具体实现不同,但它们都有共同能力:
- 支付;
- 退款。
这时可以定义一个接口。
11.1 定义支付接口
java
interface Payment {
void pay(double amount);
void refund(double amount);
}
11.2 微信支付实现接口
java
class WeChatPay implements Payment {
@Override
public void pay(double amount) {
System.out.println("使用微信支付:" + amount + " 元");
}
@Override
public void refund(double amount) {
System.out.println("微信退款:" + amount + " 元");
}
}
11.3 支付宝支付实现接口
java
class AliPay implements Payment {
@Override
public void pay(double amount) {
System.out.println("使用支付宝支付:" + amount + " 元");
}
@Override
public void refund(double amount) {
System.out.println("支付宝退款:" + amount + " 元");
}
}
11.4 订单服务只依赖接口
java
class OrderService {
public void checkout(Payment payment, double amount) {
payment.pay(amount);
}
}
11.5 测试代码
java
public class Test {
public static void main(String[] args) {
OrderService orderService = new OrderService();
Payment weChatPay = new WeChatPay();
Payment aliPay = new AliPay();
orderService.checkout(weChatPay, 100);
orderService.checkout(aliPay, 200);
}
}
输出结果:
text
使用微信支付:100.0 元
使用支付宝支付:200.0 元
12. 为什么这个支付案例适合用接口?
因为订单服务并不关心具体是哪种支付方式。
它只关心一件事:
java
payment.pay(amount);
只要传进来的对象实现了 Payment 接口,就可以完成支付。
以后新增银行卡支付:
java
class BankCardPay implements Payment {
@Override
public void pay(double amount) {
System.out.println("使用银行卡支付:" + amount + " 元");
}
@Override
public void refund(double amount) {
System.out.println("银行卡退款:" + amount + " 元");
}
}
原来的 OrderService 不需要修改。
java
Payment bankCardPay = new BankCardPay();
orderService.checkout(bankCardPay, 300);
这符合一个重要设计原则:
对扩展开放,对修改关闭。
也就是说,新增功能时,尽量新增代码,而不是修改老代码。
13. 真实开发场景:模板方法适合用抽象类
假设我们要设计一个"做饭流程"。
不同菜的做法不同,但整体流程差不多:
- 准备食材;
- 开火;
- 烹饪;
- 装盘。
其中"开火"和"装盘"可能是公共步骤,而"准备食材"和"烹饪"因菜而异。
这种情况适合用抽象类。
13.1 定义抽象类
java
abstract class CookTemplate {
public final void cook() {
prepareFood();
fire();
doCook();
dishUp();
}
public abstract void prepareFood();
public void fire() {
System.out.println("开火");
}
public abstract void doCook();
public void dishUp() {
System.out.println("装盘");
}
}
这里的 cook() 方法定义了固定流程。
java
public final void cook() {
prepareFood();
fire();
doCook();
dishUp();
}
final 表示不希望子类修改整体流程。
13.2 具体菜品继承抽象类
java
class TomatoEgg extends CookTemplate {
@Override
public void prepareFood() {
System.out.println("准备番茄和鸡蛋");
}
@Override
public void doCook() {
System.out.println("炒番茄鸡蛋");
}
}
java
class BeefNoodle extends CookTemplate {
@Override
public void prepareFood() {
System.out.println("准备牛肉和面条");
}
@Override
public void doCook() {
System.out.println("煮牛肉面");
}
}
13.3 测试代码
java
public class Test {
public static void main(String[] args) {
CookTemplate tomatoEgg = new TomatoEgg();
tomatoEgg.cook();
System.out.println("------");
CookTemplate beefNoodle = new BeefNoodle();
beefNoodle.cook();
}
}
输出结果:
text
准备番茄和鸡蛋
开火
炒番茄鸡蛋
装盘
------
准备牛肉和面条
开火
煮牛肉面
装盘
14. 为什么这个做饭案例适合用抽象类?
因为这里有明显的父子关系和公共流程:
text
番茄炒蛋 是一种 做饭流程
牛肉面 是一种 做饭流程
抽象类可以做到:
- 固定整体流程;
- 复用公共方法;
- 让子类实现差异步骤。
接口更适合定义能力,而抽象类更适合定义模板和公共骨架。
15. 抽象类和接口的核心区别
| 对比点 | 抽象类 | 接口 |
|---|---|---|
| 核心思想 | 描述"是什么" | 描述"能做什么" |
| 关系 | is-a 关系 |
has-a-capability 能力关系 |
| 关键字 | abstract class |
interface |
| 使用方式 | extends |
implements |
| 继承数量 | 一个类只能继承一个抽象类 | 一个类可以实现多个接口 |
| 成员变量 | 可以有普通变量 | 默认是 public static final 常量 |
| 构造方法 | 可以有构造方法 | 不能有构造方法 |
| 普通方法 | 可以有普通方法 | Java 8 后可以有 default 方法和 static 方法 |
| 主要作用 | 代码复用、模板设计、父子关系 | 规范约束、能力扩展、解耦合 |
16. 如何判断该用抽象类还是接口?
可以问自己两个问题。
16.1 这些类是不是同一种东西?
如果是,优先考虑抽象类。
例如:
text
Dog 是 Animal
Cat 是 Animal
Student 是 Person
Teacher 是 Person
适合:
java
abstract class Animal {}
abstract class Person {}
16.2 这些类是不是只是有相同能力?
如果是,优先考虑接口。
例如:
text
Bird 可以 fly
Airplane 可以 fly
Superman 可以 fly
适合:
java
interface Flyable {}
17. 一个完整的综合例子
我们设计一个校园系统。
学校里有老师和学生:
- 老师是人;
- 学生也是人;
- 老师可以教学;
- 学生可以学习;
- 有些老师会开车;
- 有些学生也会开车。
这时可以这样设计:
17.1 抽象类 Person
java
abstract class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void sleep() {
System.out.println(name + " 正在睡觉");
}
public abstract void duty();
}
17.2 接口 Drivable
java
interface Drivable {
void drive();
}
17.3 Teacher 类
java
class Teacher extends Person implements Drivable {
public Teacher(String name, int age) {
super(name, age);
}
@Override
public void duty() {
System.out.println(name + " 的职责是教学");
}
@Override
public void drive() {
System.out.println(name + " 开车去学校");
}
}
17.4 Student 类
java
class Student extends Person {
public Student(String name, int age) {
super(name, age);
}
@Override
public void duty() {
System.out.println(name + " 的职责是学习");
}
}
17.5 测试代码
java
public class Test {
public static void main(String[] args) {
Person teacher = new Teacher("王老师", 40);
Person student = new Student("小明", 18);
teacher.sleep();
teacher.duty();
student.sleep();
student.duty();
Drivable driver = new Teacher("李老师", 38);
driver.drive();
}
}
输出结果:
text
王老师 正在睡觉
王老师 的职责是教学
小明 正在睡觉
小明 的职责是学习
李老师 开车去学校
18. 最终总结
抽象类的本质:
抽象类是为了抽取同一类事物的共同属性和行为。
接口的本质:
接口是为了抽取不同事物的共同能力和规范。
最简单的判断方式:
text
如果你想表达:A 是一种 B
用抽象类
如果你想表达:A 具备某种能力
用接口
例如:
java
class Dog extends Animal
表示:
text
狗是一种动物
java
class Bird implements Flyable
表示:
text
鸟具备飞行能力
19. 记忆口诀
抽象类看身份,接口看能力。
抽象类重复用,接口重扩展。
抽象类是父子关系,接口是能力规范。
20. 面试回答模板
如果面试官问:"抽象类和接口有什么区别?"
可以这样回答:
抽象类和接口都可以实现抽象设计,但它们解决的场景不同。抽象类主要用于描述同一类事物的共同特征,强调的是"是什么",适合有父子关系、需要代码复用的场景。比如
Dog extends Animal,表示狗是一种动物。接口主要用于描述某种能力或规范,强调的是"能做什么",适合不同类型对象具有相同能力的场景。比如Bird implements Flyable,表示鸟具备飞行能力。Java 中一个类只能继承一个抽象类,但可以实现多个接口,所以接口更适合能力扩展和系统解耦。