接口和抽象类的区别是什么?怎么选择?
结论:抽象类是"is-a 的部分实现"——单继承、可以有构造器、实例字段、任意可见性的方法与状态,适合复用代码和定义模板方法;接口是"can-do 的能力契约"——多实现、字段隐式为 public static final、方法在 JDK 8 后可以有 default/static、JDK 9 后可以有 private 方法,适合跨继承体系声明能力。选择标准:要共享状态或骨架逻辑用抽象类,要定义可插拔的能力用接口;两者常常配合(接口 + 抽象骨架类)。
1. 逐项对比
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 继承/实现 | 类只能 extends 一个 | 类可以 implements 多个 |
| 构造器 | 有(供子类调用) | 无 |
| 实例字段 | 任意类型、任意可见性 | 只能是 public static final 常量 |
| 方法 | 抽象、具体、private、protected 都可 | 默认 public abstract;JDK 8 起 default/static;JDK 9 起 private |
| 多态方式 | 继承 + 重写 | 实现 + 重写 |
| 设计意图 | 代码复用、模板方法、受控扩展 | 契约、解耦、能力组合 |
| 演化能力 | 加抽象方法会破坏所有子类 | 加 default 方法不破坏实现类 |
| 典型例子 | AbstractList、InputStream | List、Comparable、AutoCloseable |
2. 结构差异示意
抽象类: 接口:
Animal Flyable Swimmable
├─ name 字段 (无状态) (无状态)
├─ 构造器 ↑ ↑
├─ eat() 具体实现 └──── Duck ───┘ ← 可同时实现多个
└─ sound() 抽象
↑
Dog(单继承)
接口不能有实例状态这一点,正是它支持多实现的根本原因:没有状态就没有"菱形继承时字段归谁"的问题。
3. 代码:接口打契约,抽象类收公共逻辑
public interface PayChannel {
String code(); // 抽象方法
default boolean support(String currency) { // 默认实现,实现类可不写
return "CNY".equals(currency);
}
static PayChannel of(String code) { // 静态工厂:按编码找实现
return ServiceLoader.load(PayChannel.class).stream()
.map(ServiceLoader.Provider::get)
.filter(p -> p.code().equals(code))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("unknown: " + code));
}
}
public abstract class AbstractPayChannel implements PayChannel {
protected final RetryTemplate retry; // 共享状态
protected AbstractPayChannel(RetryTemplate retry) {
this.retry = retry;
}
protected abstract void doPay(long cents); // 子类实现
public final void pay(long cents) { // 模板方法:固定流程
retry.execute(() -> doPay(cents));
}
}
public class AlipayChannel extends AbstractPayChannel {
public AlipayChannel(RetryTemplate retry) { super(retry); }
@Override public String code() { return "alipay"; }
@Override protected void doPay(long cents) { /* 调用支付宝 SDK */ }
}
这里接口负责"能被支付框架识别",抽象类负责"重试 + 模板流程",职责清晰。pay() 用 final 锁住流程,子类只能填 doPay() 这一空——这就是模板方法模式(Template Method)。
4. 选择清单
| 需求 | 选择 |
|---|---|
| 多个实现共享字段/构造逻辑 | 抽象类 |
| 定义一组能力,允许一个类同时具备多个 | 接口 |
| 未来可能加方法又不想破坏实现类 | 接口 + default 方法 |
需要 protected 方法给子类用 | 抽象类 |
| 需要被远程调用/序列化的契约 | 接口 |
| 框架扩展点(SPI) | 接口 |
| 既要契约又要复用 | 接口 + 抽象骨架类(如 AbstractList) |
5. 常见追问
| 追问 | 回答要点 |
|---|---|
| 接口为什么不能有构造器? | 接口不能被实例化,没有实例状态需要初始化 |
| 抽象类可以没有抽象方法吗? | 可以,此时只是"不能被实例化"的普通类 |
| 接口可以继承多个接口吗? | 可以,interface C extends A, B;类实现 C 时要处理冲突 |
| 抽象类可以实现接口吗? | 可以,且可以不实现接口方法,留给子类 |
default 方法冲突怎么解决? | 实现类必须重写,或用 A.super.method() 显式指定 |
| 接口里的字段为什么必须是常量? | 接口描述契约、不持有状态;public static final 是编译期常量,可安全共享 |
抽象类能 final 吗? | 不能,final 与 abstract 互斥 |
小结
抽象类解决"复用",接口解决"解约"。一个实用判断:如果子类之间需要共享状态或固定流程,用抽象类;如果只是想让不同家族的类都具备某种能力,用接口。现代 Java 更偏向"接口 + 组合",因为继承是强耦合,接口演化也更安全。