继承表示“是一个”(is-a)关系,子类获得父类非私有成员并遵守里氏替换原则;组合表示“有一个”(has-a)关系,通过字段复用行为,更灵活、低耦合、易测试。

继承关系在Java里意味着什么
继承表示“是一个”(is-a)关系,子类天然获得父类的非私有成员,同时承担父类的契约责任。Java中用 extends 实现单继承,且子类必须满足里氏替换原则——任何父类能出现的地方,子类也必须能安全替代。
常见错误是把“能用”当成“该用”:比如为复用几个方法就让 Car 继承 Engine,这违反常识(汽车不是引擎),运行时可能触发 ClassCastException 或逻辑错位。
- 父类
protected成员可被子类直接访问,但包外不可见 -
final类不能被继承,final方法不能被重写 - 构造器不继承,但子类构造器默认隐式调用
super(),若父类无无参构造器,必须显式写super(...)
组合关系更贴近真实建模
组合表达“有一个”(has-a)关系,通过在类中声明其他类的实例变量来复用行为。它不产生强耦合,也不要求语义上存在类型兼容性。
例如 Order 类持有 PaymentProcessor 引用,而不是继承它——支付方式可以随时切换(微信、支付宝、MockProcessor),而继承会把实现细节锁死在类型系统里。
立即学习“Java免费学习笔记(深入)”;
- 组合对象生命周期通常由宿主类控制,但也可交由外部管理(如依赖注入)
- 没有语言级语法糖,靠字段 + 构造器/Setter + 委托方法实现
- 支持运行时替换,天然适配策略模式、装饰器等设计模式
为什么编译期检查过不了,但组合能绕开
继承强制编译器验证类型兼容性,一旦父类签名变更(如加了 abstract 方法),所有子类必须响应;而组合只依赖接口或具体类型的公开API,只要方法存在且签名匹配,就不报错。
典型陷阱是误用继承模拟状态机:比如用 RunningState、PausedState 继承 GameState,结果发现状态切换需要频繁强制转型,且无法共享状态数据。改用组合后,Game 持有 GameState 接口引用,切换只需赋值新实例。
- 继承导致子类暴露父类实现细节,破坏封装(如父类字段被子类意外修改)
- 组合可配合
private字段 + 公共委托方法,隐藏内部对象的创建和使用逻辑 - 单元测试时,组合对象容易被 Mock,继承链越深,Mock 越难写
选继承还是组合?看这三个信号
真正决定因素不是“能不能”,而是“该不该”。当出现以下任一情况,优先选组合:
- 想复用代码,但两个类之间不存在自然的 is-a 关系(如
Logger和UserService) - 需要在运行时动态改变行为(如不同环境加载不同配置解析器)
- 父类不是为继承设计的——比如没把方法设为
protected,也没标注@Override可重写点,甚至用了大量final修饰
继承不是坏设计,但它比组合更重、更刚性。一个常被忽略的细节是:Java 的 Object 已经是所有类的隐式父类,你不需要再靠继承去“获得通用能力”,缺功能就加字段、加方法、加接口实现。










