组合能避免继承的脆弱性,因它仅依赖接口或公开行为而非父类实现细节,不暴露内部方法,支持多组件组合、运行时策略替换,并需注意生命周期管理和所有权;而继承仅适用于明确设计为可扩展的is-a关系场景。

为什么组合能避免继承带来的脆弱性
继承让子类紧耦合父类的实现细节,一旦父类修改 protected 方法或构造逻辑,所有子类可能意外崩坏。组合则只依赖接口或明确公开的行为,比如用 java.util.List 字段代替继承 ArrayList,父类内部扩容策略怎么变,都不影响你的类。
- 继承暴露实现:子类可能误用
super.someInternalMethod(),而该方法本不该被外部调用 - 组合控制契约:你决定调用
list.add()还是list.get(),不继承一堆无关的ensureCapacity()、trimToSize() - 单继承限制:Java 不支持多继承,但可以同时持有
DataSource、Validator、Logger多个组件实例
如何用组合替代模板方法模式
传统继承式模板方法(如 AbstractList 的 get(int) 抽象)强制子类继承骨架。换成组合后,把算法步骤抽成函数式接口,由外部注入行为。
public class DataProcessor {
private final Function preprocessor;
private final Predicate validator;
public DataProcessor(Function preprocessor,
Predicate validator) {
this.preprocessor = preprocessor;
this.validator = validator;
}
public String process(String input) {
String processed = preprocessor.apply(input);
if (!validator.test(processed)) {
throw new IllegalArgumentException("Invalid after preprocessing");
}
return processed;
}
}
- 测试更简单:直接传入
s -> s.toUpperCase()和s -> s.length() > 0即可单元验证 - 运行时可换策略:同一实例能通过构造参数切换校验规则,无需创建新子类
- 避免“伪继承”陷阱:不用为了复用某段逻辑就生造一个
BaseDataProcessor类再继承它
组合中如何处理生命周期与所有权
组合不是简单加个字段就完事——谁创建、谁关闭、是否共享,直接影响资源泄漏和线程安全。
- 若字段是
java.io.Closeable(如BufferedReader),应在本类的close()中显式调用其close(),否则资源不会释放 - 避免把外部传入的共享对象(如全局
ExecutorService)当成私有资源去shutdown() - 构造参数建议用
final修饰,防止中途被替换成不兼容实现(例如传入非线程安全的HashMap替代预期的ConcurrentHashMap) - 必要时提供
withXxx(...)构建器方法,而不是暴露 setter,防止状态不一致
什么时候仍该用继承
不是所有继承都要消灭。当存在清晰的「是-一种(is-a)」关系,且父类明确设计为被继承(标注 @SuppressWarnings("serial")、提供 protected 钩子、文档写明扩展点),才考虑继承。
立即学习“Java免费学习笔记(深入)”;
- 典型合理场景:自定义异常继承
RuntimeException;Swing 组件扩展JPanel并重写paintComponent() - 反例:为复用
HashMap的put()就继承它——这违反 is-a,应组合Map接口 - 关键判断依据:看 Javadoc 是否出现 “Subclasses may…” 或 “This method is intended to be overridden”
组合不是银弹,它的优势在于可控、可测、可替换;而继承的代价常被低估——尤其是当父类来自第三方库时,一次小版本升级就可能让你的子类抛出 IllegalAccessError 或行为突变。










