接口比抽象类更适合定义契约,因其强制零状态、零实现,天然防止实现细节泄漏;抽象类易引入字段、构造行为等隐式绑定;接口支持多实现,命名更聚焦调用方需求,利于解耦。
因为接口只声明“能做什么”,不掺杂任何实现细节、状态或默认逻辑,而抽象类容易悄悄带入字段、构造行为或具体方法,让契约变味。
接口强制“零状态、零实现”
接口里只能有常量(public static final)和方法签名(Java 8+ 允许 default 和 static 方法,但它们不改变契约本质)。它不许定义普通字段、不许写构造函数、不许带 protected 成员——这些限制天然防止实现细节泄漏。比如:
-
PaymentProcessor 接口只写
process(double amount)和refund(String txId) - 调用方完全不知道背后是支付宝、银联还是 Mock 对象
- 只要返回值符合约定,流程就继续,不依赖任何内部结构
抽象类自带隐式绑定风险
抽象类可以有字段(如 protected Logger logger)、可以有非抽象方法(如 formatLog())、甚至可以控制子类初始化顺序。这些都会让子类被动继承不该承担的职责:
- 新增一个日志级别字段,所有子类都得面对这个状态
- 重写一个默认格式方法,可能破坏原有语义
- 测试时必须构造完整继承链,Mock 成本高
多实现能力体现契约的通用性
一个类可以同时实现 UserRepository、Cacheable、Auditable 多个接口,说明它在不同维度上履行不同契约。这种正交性是抽象类无法提供的——Java 只允许单继承,一旦选了某个抽象父类,就锁死了扩展路径。
- FileUserRepo 和 RedisUserRepo 都实现 UserRepository,上层代码无感知
- 但若都继承 BaseUserRepo 抽象类,就得共享它的字段和生命周期逻辑
- 换存储时,不是“换实现”,而是“改继承”,耦合度陡增
命名与边界更聚焦“谁要什么”
接口名天然导向契约意图:Logger、Validator、Serializer——每个词都在回答“调用方需要什么能力”。而抽象类名往往暗示“这是一个什么东西”,比如 BaseJsonProcessor 或 AbstractPaymentHandler,已经悄悄把实现方式写进名字里。
- 当你要加缓存功能,可以新增 Cacheable 接口,不影响原有契约
- 但如果放在抽象类里,很可能得改基类、发版、通知所有子类适配
- 守住接口边界,就是守住解耦的底线

















