接口定义“能做什么”,抽象类定义“是什么、怎么起步”;需多继承选接口,需共享状态或构造逻辑选抽象类,现代推荐“接口为主+小抽象类收口”分层设计。
接口和抽象类不是“选哪个更好”,而是“哪个更贴合当前设计意图”。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 方法可被任意重写,失去控制力default 方法看似让接口“变灵活”,但它会引发菱形继承冲突、语义模糊和测试盲区。
default 方法,实现类必须显式覆写,否则编译失败——这不是设计选择,是强制补救default 方法无法访问实现类的私有字段或 this 引用以外的状态,容易写出“假复用”(表面共用,实则每个实现都要重新查一遍数据库)default 方法的 AOP 支持有限(比如 @Transactional 不生效),容易误以为加了就自动受管典型模式是:顶层用接口描述契约(如 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) {
// 只写支付宝特有调用逻辑
}
}
这种分层把协议、骨架、细节拆开,比强行塞进一个抽象类或全靠接口 def
ault 更易维护。最容易被忽略的是:抽象类一旦发布,字段和构造器变更几乎等于破坏性升级;而接口加 default 方法虽二进制兼容,却可能悄悄改变行为语义——上线前务必确认所有实现类是否真能接受那个默认逻辑。