接口定义“能做什么”,抽象类定义“是什么、怎么起步”;需多继承选接口,需共享状态或构造逻辑选抽象类,现代推荐“接口为主+小抽象类收口”分层设计。

接口和抽象类不是“选哪个更好”,而是“哪个更贴合当前设计意图”。Java 8 之后接口支持 default 和 static 方法,模糊了边界,但语义责任没变:接口定义「能做什么」,抽象类定义「是什么、怎么起步」。
当需要多继承能力时,必须用接口
Java 类只能单继承,但可以实现多个接口。如果你的设计天然要求一个类同时具备多种可组合的能力(比如 Runnable + Serializable + Comparable),抽象类无法满足。
- 抽象类不能被多继承,即使只写一个
extends,也锁死了父类位置 - 接口之间可以用
extends多继承(如interface List),而抽象类做不到extends Collection , Cloneable, Iterable - 如果未来可能让不相关的类共享某组行为(比如
Loggable、Retryable),接口是唯一可插拔方案
当需要共享状态或构造逻辑时,优先考虑抽象类
接口不能有实例字段,也不能有带逻辑的构造器;抽象类可以。如果你发现多个子类反复初始化相同字段、复用相同校验流程,或者需要强制执行某个初始化顺序,抽象类更合适。
- 抽象类可声明
protected字段(如protected final Logger logger),接口不行 - 抽象类可提供带参数的构造器,并在其中调用
init()或抛出检查异常,接口无构造器概念 - 抽象类中的
final方法能防止子类绕过关键流程(比如先校验再执行),接口的default方法可被任意重写,失去控制力
当要升级已有 API 且保持兼容时,谨慎使用 default 方法
default 方法看似让接口“变灵活”,但它会引发菱形继承冲突、语义模糊和测试盲区。
立即学习“Java免费学习笔记(深入)”;
- 若两个接口都定义了同签名的
default方法,实现类必须显式覆写,否则编译失败——这不是设计选择,是强制补救 -
default方法无法访问实现类的私有字段或this引用以外的状态,容易写出“假复用”(表面共用,实则每个实现都要重新查一遍数据库) - Spring 等框架对
default方法的 AOP 支持有限(比如@Transactional不生效),容易误以为加了就自动受管
现代 Java 项目中更倾向“接口为主 + 小抽象类收口”
典型模式是:顶层用接口描述契约(如 PaymentService),中间放一个 AbstractPaymentService 提供通用日志、幂等校验、异常转换,具体实现类只专注业务分支逻辑。
public interface PaymentService {
Result pay(Order order);
}
public abstract class AbstractPaymentService implements PaymentService {
protected final Metrics metrics;
protected AbstractPaymentService(Metrics metrics) {
this.metrics = metrics;
}
@Override
public Result pay(Order order) {
metrics.count("payment.attempt");
try {
return doPay(order); // 模板方法,由子类实现
} catch (Exception e) {
metrics.count("payment.fail");
throw e;
}
}
protected abstract Result doPay(Order order);
}
public class AlipayService extends AbstractPaymentService {
@Override
protected Result doPay(Order order) {
// 只写支付宝特有调用逻辑
}
}
这种分层把协议、骨架、细节拆开,比强行塞进一个抽象类或全靠接口 default 更易维护。最容易被忽略的是:抽象类一旦发布,字段和构造器变更几乎等于破坏性升级;而接口加 default 方法虽二进制兼容,却可能悄悄改变行为语义——上线前务必确认所有实现类是否真能接受那个默认逻辑。










