2.2 继承误用与组合改造 本节摘要:继承是最强的耦合。本节从 JDK 自身的反面教材( 、 )讲到一个业务系统里"基类加一个字段、三个子系统崩"的真实事故,给出 is-a/has-a 判据、组合+委托的改造手法,以及模板方法与接口默认方法的适用边界。 从 JDK 自己踩过的坑说起 Java 标准库里有两个流传最广的继承反面教材。 继承 ,于是它"是一个"动态数组—— / 之外, 、 全部裸露,任何调用方都能从栈中间插一个元素,后进先出的不变量形同虚设。 继承 (键值都被声明为 ),但 没被禁止,一个不走 的调用就能让后续 在类型转换上崩溃。 这两个类的教训是同一条:继承意味着无条件接受父类的全部实现细节,包括你没打算要的那部分。子类与父类的耦合不是"语义上像",而是"实现上焊死"。
本节摘要:继承是最强的耦合。本节从 JDK 自身的反面教材(
Stack extends Vector、Properties extends Hashtable)讲到一个业务系统里"基类加一个字段、三个子系统崩"的真实事故,给出 is-a/has-a 判据、组合+委托的改造手法,以及模板方法与接口默认方法的适用边界。
Java 标准库里有两个流传最广的继承反面教材。
java.util.Stack 继承 Vector,于是它"是一个"动态数组——push/pop 之外,get(int)、insertElementAt 全部裸露,任何调用方都能从栈中间插一个元素,后进先出的不变量形同虚设。Properties 继承 Hashtable(键值都被声明为 String),但 put(Object,Object) 没被禁止,一个不走 setProperty 的调用就能让后续 getProperty 在类型转换上崩溃。
这两个类的教训是同一条:继承意味着无条件接受父类的全部实现细节,包括你没打算要的那部分。子类与父类的耦合不是"语义上像",而是"实现上焊死"。
某电商的结算链路里有个 BaseRequest,三年间被短信、push、结算三个子系统继承。某次迭代,结算侧需要加一个 settleCurrency 字段,顺手加在 BaseRequest 上。上线当晚:
BaseRequest 全字段算大小,命中率下跌;修复花了两小时,善后(对端灰度、缓存预热)花了三天。复盘会上达成共识:继承让"父类的私有决策"变成"全体现任与未来子类的公共约束",而做出这个决策的人往往不知道有多少子孙。
is-a 判据要看语义而非表面。"充电宝是一个电池"在物理上成立,但若你的 Battery 类暴露了 dischargeToEmpty,让充电宝继承它就是灾难。工程上更实用的判据是变更影响测试:想象给父类加一个方法、改一个语义,你能不能接受所有子类被动继承这个变化?不能,就该组合。
组合改造的手法是"持有 + 委托":
// 误用继承:WordList 把 Vector 的所有危险操作一并继承 class WordList extends Vector<String> { /* ... */ } // 组合:只暴露需要的形状 class WordList { private final List<String> words = new ArrayList<>(); public void add(String w) { words.add(w); } public String last() { return words.get(words.size() - 1); } public int count() { return words.size(); } // 不需要的能力根本不存在 误用无从谈起 }
改造后有个立竿见影的收益:接口面收窄到业务真正需要的形状。WordList 没有 get(int),就不存在"按索引越界改词"的 Bug 类别——不是被禁止,而是不存在。

要公平:继承在框架扩展点上仍是正确工具。HttpServlet 通过 doGet/doPost 留白让子类填充,模板方法模式把"流程骨架固定、步骤开放"的结构表达得非常干净。这类场景的共性是:父类是特意设计为被继承的,JDK/框架作者替你控制了父类的演化纪律(不随便加公有方法、改动有版本化承诺)。
反之,继承一个不是为继承而写的具体类,就是在读它的源码之前就签了全责协议。Effective Java 把这条总结为"要么为继承而设计并写文档,要么禁止继承"——final 类和 String 的不可变设计就是这个原则的正面示范。
Java 8 的接口默认方法提供了中间态:横切能力(如 Comparator 的组合方法)以默认方法下发,实现类选择性覆写,比"为了一个工具方法去继承工具类"干净得多。
⚠️ 常见坑:子类覆写方法时调用被父类构造器间接调用的方法。父类构造器执行时子类字段还没初始化,覆写方法读到的是默认值——"构造期调用可覆写方法"是继承里最阴的时序坑。
💡 关键直觉:继承是白盒复用(我知道你怎么实现),组合是黑盒复用(我只知道你能做什么)。黑盒的合同更稳定,所以默认选组合,继承留给真正的多态层次。
Stack/Properties 展示了继承如何暴露不该有的能力下一节进入内存维度:内部类如何把一个大对象悄悄拖住不放。