抽象类适用于“同类事物共性变化”,可封装状态与通用逻辑;接口适用于“能力自由拼装”,支持多实现与正交扩展,体现“能做什么”而非“是什么”。

初学者常以为“抽象类和接口只是语法不同”,其实它们应对的是两类完全不同的业务变化——一类是“同类事物的共性在变”,另一类是“能力组合方式在变”。选对了,后续加功能不改结构;选错了,一个小需求就可能推翻整个继承体系。
看变化是否围绕“同一类事物”展开
如果新增需求始终落在“同一种东西”的范畴内,比如:所有动物都要新增“冬眠”行为,所有支付方式都要支持“分账回调”,所有图形都要支持“导出SVG”,说明核心分类没变,只是同类行为在扩展。这时优先用抽象类。
- 抽象类能自然承载共用状态(如
name、createTime)和可复用逻辑(如统一日志记录、参数校验模板) - 子类继承后只需专注差异部分,不用重复写初始化或通用流程
- 未来若要给所有子类加一个默认实现的方法(比如
logOperation()),直接在抽象类里补上即可,所有子类自动获得
看变化是否来自“能力自由拼装”
如果业务要求越来越灵活:今天订单要支持微信支付+短信通知,明天要支持支付宝+邮件通知+风控拦截,后天又加个“海外用户需启用汇率转换”——这些能力彼此正交、可插拔、不绑定具体类型,那就该用接口。
- 一个类可以同时实现
Payable、Notifiable、RiskCheckable,互不影响 - 新加一种能力(比如
Refundable)时,只影响需要退款的类,其他类完全无感 - Java 8+ 的 default 方法还能提供基础实现,避免所有实现类重复写同样逻辑
看未来是否要约束“谁可以拥有这个能力”
接口更适合表达开放契约。比如“所有能登录的模块都必须支持双因素认证”,但登录模块可能是 Web 页面、小程序、IoT 设备端——它们根本不是同类事物,硬塞进同一个抽象类会破坏语义。这时候用接口更自然。
- 接口不关心“你是什么”,只规定“你能做什么”
- 不同技术栈、不同团队开发的模块,只要实现同一接口,就能被统一调度或注入
- 测试时也容易 mock,比如用
Mockito.mock(Exportable.class)快速构造行为桩
一个判断口诀帮你快速决策
面对新需求,先问自己三句话:
- 这个功能是不是所有子类“天然就应该有”的?→ 是 → 抽象类
- 这个功能会不会被完全无关的类“按需装配”?→ 是 → 接口
- 将来加新功能时,是否希望“改一处,全生效”,还是“只影响相关类”?→ 前者倾向抽象类,后者倾向接口











