接口和抽象类有什么区别?怎么选?
简化版
抽象类是「是什么」(is-a),用来抽象一类事物的共性,可以有成员变量、构造器、具体方法,一个类只能继承一个抽象类。接口是「能做什么」(can-do),定义一组行为契约,一个类能实现多个接口。选择口诀:共享代码和状态用抽象类,定义能力和多重契约用接口。
详细版
| 维度 | 抽象类 abstract class | 接口 interface |
|---|---|---|
| 继承/实现 | 单继承(extends 一个) | 多实现(implements 多个) |
| 成员变量 | 普通成员变量都行 | 只能是 public static final 常量 |
| 方法 | 抽象方法 + 具体方法都行 | 抽象方法(+ 默认/静态方法,Java 8+) |
| 构造器 | 有 | 无 |
| 语义 | is-a(是一种) | can-do(能做某事) |
abstract class Animal { // 抽象类:抽象「动物」这类事物
protected String name; // 可以有状态
Animal(String name) { this.name = name; } // 可以有构造器
abstract void makeSound(); // 抽象方法,子类必须实现
void sleep() { } // 也可以有具体方法(共享实现)
}
interface Swimmable { // 接口:定义「会游泳」这个能力
void swim();
}
class Duck extends Animal implements Swimmable { // 继承一个类,实现多个接口
Duck(String name) { super(name); }
void makeSound() { }
public void swim() { }
}
完整版教学
一、核心区别:单继承 vs 多实现,状态 vs 契约
两个最本质的差异:
- 数量:Java 类只能单继承(一个 extends),但能实现多个接口。这是因为多继承会有「菱形问题」(两个父类有同名方法,子类不知道用谁),Java 用「单继承类 + 多实现接口」规避了它。
- 能不能有状态和实现:抽象类可以有成员变量(状态)、构造器、完整的方法实现,能沉淀公共代码;接口传统上只有契约(方法签名),侧重定义能力。
可以这样把握:抽象类是「模板」,管一批子类的共同结构和代码;接口是「协议」,管某种能力的对外约定。
二、语义之别:is-a vs can-do
这是选型的关键判断依据:
- 用
extends抽象类表达 is-a:Dog is-a Animal(狗是一种动物)——它们有本质的分类归属关系。 - 用
implements接口表达 can-do:Duck can Swim(鸭子会游泳)——这是一种能力,可以跨越分类。Fish也能Swimmable,但 Fish 和 Duck 不是同一类动物。
所以「游泳」这种横跨不同类的能力适合做接口;「动物」这种本质分类适合做抽象类。
三、Java 8+ 之后,接口越来越强
Java 8 给接口加了 default 默认方法和 static 静态方法,接口也能有方法实现了,两者界限变模糊,但根本区别仍在:
- default 方法:让接口在新增方法时不破坏已有实现类(否则所有实现类都要改),也能提供一些通用实现(如
List.sort())。 - 接口仍不能有实例状态:default 方法里不能用实例字段,因为接口没有成员变量。这是它和抽象类始终的分界——接口无状态,抽象类有状态。
补充:Java 9 接口还能有
private方法(给 default 方法复用),但仍然无实例字段。
四、怎么选——决策清单
- 需要多个不相关的类共享一组行为契约 → 接口(能多实现)。
- 需要在一批同类子类间共享代码和状态(字段、构造逻辑、模板方法)→ 抽象类。
- 定义能力/角色(Comparable、Runnable、Serializable)→ 接口。
- 定义一类事物的骨架,且想强制子类走某个流程(模板方法模式)→ 抽象类。
- 拿不准时优先接口——它更灵活(类只有一次单继承的机会,别轻易花掉),需要共享实现时再上抽象类,两者也常配合(接口定契约 + 抽象类给部分默认实现,如
List/AbstractList)。
五、默认方法冲突与 API 演进
接口支持多实现后,两个父接口可能提供同签名 default 方法。Java 不会猜测应该选哪一个,而是要求实现类显式重写,从而把冲突决策留给最了解业务的类;若要复用某一方实现,可以写 A.super.run()。这避免了传统多继承中状态和实现来源不明的菱形问题。
interface A { default String name() { return "A"; } }
interface B { default String name() { return "B"; } }
class C implements A, B {
@Override public String name() {
return A.super.name() + B.super.name(); // 显式决定组合方式
}
}
接口的 default 方法主要服务兼容演进:一个接口已有 100 个实现类时,新增抽象方法会让 100 个类同时编译失败;提供合理默认实现则能逐步升级。抽象类新增具体方法也能兼容子类,但它还能依赖受保护状态,因此更容易与继承层次耦合。
| 变化 | 接口 | 抽象类 |
|---|---|---|
| 新增抽象方法 | 破坏全部现有实现 | 破坏具体子类 |
| 新增有实现的方法 | default 可兼容,但要处理冲突 | 通常兼容,并可访问共享状态 |
| 同时采用多个能力 | 可以多实现 | 不能多继承类 |
| 保存每个实例的公共状态 | 不适合 | 适合 |
六、常见误区与追问
- 误区:接口从 Java 8 起等同抽象类。 接口仍没有实例状态和构造器,而且可以多实现,语义并未合并。
- 误区:接口字段可以是普通实例字段。 接口字段隐式都是
public static final,属于常量而非对象状态。 - 误区:为复用两行代码就建立抽象父类。 单继承会形成强耦合,简单复用也可以通过组合完成。
- 追问:两个接口 default 方法冲突怎么办? 实现类必须重写,并可用
InterfaceName.super.method()指定来源。 - 追问:抽象类能否实现接口? 可以,而且常用来提供部分默认实现,让具体子类只补剩余行为。
- 追问:接口能否有 private 方法? Java 9 起可以,供接口内部 default/static 方法复用,但仍不能成为实例状态。
选择心法:先问这是“稳定分类骨架”还是“可组合能力契约”,再问是否必须共享状态;不要先看哪一种少写代码。
一个常见组合是 JDK 集合体系:List 定义可替换的行为契约,AbstractList 提供骨架实现,ArrayList 再选择数组作为具体状态。调用方依赖 List,实现者可按需要继承骨架,也可以完全绕开抽象类自行实现接口。
调用方 → List 接口
↑
AbstractList(可选骨架)
↑
ArrayList(具体状态与算法)
这说明“优先接口”不是禁止抽象类,而是让抽象类成为实现便利设施,不把调用方锁进某一条继承链。
七、加强记忆
抽象类代表一条单继承的分类骨架,能保存状态、运行构造逻辑并沉淀模板实现;接口代表可跨越分类组合的能力契约,没有实例状态,却能多实现并用 default 方法平滑演进。默认方法没有消除二者边界,反而要求显式处理多接口冲突。工程上优先用接口保持替换性,需要同族状态和受保护扩展点时才引入抽象类,两者也可以“接口定契约、抽象类给骨架”配合使用。