继承易导致耦合,因强制绑定父类实现细节,子类被动继承冗余成员且受父类变更影响;组合通过接口委托和final字段实现松耦合、可替换与可测试。

为什么 extends 一用就容易出耦合问题
继承强制绑定父类的实现细节,子类会无意识继承不需要的方法、字段,甚至被父类的修改“拖垮”。比如父类加了个 protected 字段,所有子类立刻多出一个隐式依赖;父类重构 toString(),子类输出格式可能意外崩坏。
组合则只暴露明确需要的接口,调用方只依赖契约(如 DataSource 接口),不关心背后是 HikariDataSource 还是 MockDataSource。
- 父类改
final方法?子类直接编译失败 - 父类加新构造函数?子类
super(...)全得跟着改 - 想复用父类某一部分逻辑?只能继承整个类,哪怕 90% 用不上
怎么用组合写出可替换、可测试的代码
核心是把“拥有什么”变成字段,把“能做什么”变成接口委托。不是 class OrderService extends DatabaseHelper,而是 class OrderService { private final DataSource dataSource; }。
示例:替代继承式日志工具
立即学习“Java免费学习笔记(深入)”;
// ❌ 错误:继承强耦合
class PaymentService extends Logger {
void process() {
log("start"); // 继承来的,但无法替换成 SLF4J 或 Log4j2
}
}
// ✅ 正确:组合 + 接口
interface LogSink {
void info(String msg);
}
class PaymentService {
private final LogSink logger;
PaymentService(LogSink logger) { this.logger = logger; }
void process() { logger.info("start"); }
}
- 测试时传入
new MockLogSink(),不用启动真实日志系统 - 上线换日志框架?只改构造参数,不碰业务逻辑
- 字段声明为
final,避免运行时被意外替换
protected 字段和方法是继承滥用的高发区
只要类里出现 protected 成员,基本就是在为继承铺路——而这条路往往只被一个子类走一次,却让父类从此不敢动它。
更糟的是,protected 打破封装:子类能直接读写父类内部状态,导致 “父类字段被子类悄悄改掉,父类自己还不知道” 的经典 bug。
- 把
protected String url;改成私有字段 +getUrl()只读方法 - 把
protected void doInit()提取为接口回调,由组合对象实现 - IDE 搜索
protected能快速定位继承污染点
什么时候真该用继承
极少情况:能严格满足 Liskov 替换原则(LSP)且父类是专为继承设计的 abstract class。典型如 java.util.AbstractList —— 它明确要求子类实现 get(int) 和 size(),其余方法全靠组合逻辑推导,且文档写清了契约。
- 父类没有
public或protected字段 - 所有
protected方法都有清晰不变量约束(比如“必须在init()后调用”) - 子类不重写任何非抽象方法(否则大概率违反 LSP)
现实中,多数所谓“父子关系”其实是“使用关系”,硬套继承只会让类越来越难维护。组合不是银弹,但它把“变”的部分锁进字段,把“不变”的契约留在接口里——这才是可控的复用。









