Java 代码复用:继承与组合的艺术
在面向对象编程中,代码复用是提升开发效率、降低维护成本的核心手段。Java 中实现代码复用的两种主要方式是继承(Inheritance) 和组合(Composition),二者各有适用场景,理解其差异与原理是写出高质量代码的关键。
代码复用的三种关系
在 UML 设计中,类之间的复用关系主要分为三类,核心区别在于 “依赖强度” 和 “语义关系”:
关系类型 | 关键字 | 语义描述 | 示例 |
---|---|---|---|
继承 | extends |
is-a(是一个):子类是父类的特殊类型 | 汽车(Car)是一种交通工具(Vehicle) |
组合 | 成员变量 | has-a(有一个):新类包含现有类的对象 | 汽车(Car)有一个发动机(Engine) |
依赖 | 方法参数 / 局部变量 | uses-a(使用一个):临时使用其他类 | 汽车(Car)使用汽油(Gasoline) |
继承(Inheritance):is-a 关系
继承是通过 extends
关键字让子类(Subclass)继承父类(Superclass)的属性和方法,从而复用父类代码。其核心是 “特殊化”—— 子类是父类的更具体版本。
继承的核心特性
- 代码复用:子类自动拥有父类的非私有(
public
/protected
/ 包访问)属性和方法。 - 方法重写(Override):子类可重写父类方法,修改或扩展其行为(需满足 “两同两小一大” 规则:方法名、参数列表相同;返回值、异常范围更小;访问权限更大)。
- 单继承限制:Java 中类仅支持单继承(一个子类只能有一个直接父类),避免 “菱形继承” 导致的歧义。
继承中的构造器调用
子类构造器必须先调用父类构造器,确保父类初始化完成,具体规则:
- 若子类构造器未显式调用父类构造器,编译器会自动插入
super()
(调用父类无参构造器)。 - 若父类无无参构造器,子类必须显式通过
super(参数)
调用父类有参构造器(否则编译报错)。
1 | class Parent { |
super
关键字的作用
- 调用父类构造器:
super(参数)
显式调用父类构造器(仅能在子类构造器第一行使用)。 - 访问父类成员:
super.属性
或super.方法()
可访问父类的非私有属性和方法(常用于子类重写父类方法后仍需调用父类逻辑)。
1 | class Parent { |
多态(Polymorphism):继承的灵魂
多态是指 “同一操作作用于不同对象时产生不同结果”,是继承的核心应用,其实现依赖三个条件:继承、方法重写、父类引用指向子类对象。
向上转型(Upcasting)
将子类对象赋值给父类引用,是多态的常见表现形式,编译器视为 “安全操作”(子类至少拥有父类的所有功能)。
1 | class Instrument { // 父类:乐器 |
- 为什么能调用子类方法?:Java 采用动态绑定(后期绑定),运行时根据对象实际类型(
Wind
)调用对应方法,而非编译时类型(Instrument
)。
instanceof
与向下转型
instanceof
:判断对象实际类型,避免向下转型时的ClassCastException
。- 向下转型(Downcasting):将父类引用转回子类类型,需显式强制转换(仅当父类引用指向的是子类对象时合法)。
1 | public static void main(String[] args) { |
方法绑定:静态 vs 动态
- 静态绑定:编译时确定调用哪个方法,适用于
static
、final
、private
方法(无法重写,无多态)。 - 动态绑定:运行时根据对象实际类型确定方法,适用于普通非静态方法(支持多态)。
JVM 为每个类维护方法表(记录方法签名与实际实现的映射),动态绑定时通过方法表快速查找对应方法,避免每次调用都遍历继承链。
多态的陷阱
多态仅适用于普通非静态方法,实例变量和静态方法不具备多态性:
(1)实例变量:编译时确定
实例变量的值由声明类型(而非实际类型)决定,子类和父类的同名实例变量会分别存储(子类隐藏父类变量)。
1 | class Parent { |
(2)静态方法:属于类,与对象无关
静态方法由类名直接调用,即使通过对象调用,也由声明类型决定,无多态。
1 | class Parent { |
组合(Composition):has-a 关系
组合是通过在新类中包含现有类的对象来复用代码,核心是 “聚合”—— 新类通过调用现有类的方法实现功能,而非继承其接口。
组合的实现方式
1 | class Engine { // 发动机类 |
- 语义:
Car
有一个(has-a)Engine
,通过组合复用Engine
的启动逻辑。
组合的优势
- 松耦合:新类与现有类的依赖仅通过接口(方法调用),现有类修改时只要接口不变,新类无需调整。
- 灵活性高:可动态替换组合的对象(如汽车可换不同型号的发动机),继承则无法动态改变父类。
- 避免继承的局限性:不受单继承限制,可组合多个类的功能(如汽车可同时组合发动机、变速箱、底盘等)。
组合 vs 继承:如何选择?
场景 | 优先选择 | 理由 |
---|---|---|
存在明确的 is-a 关系 | 继承 | 如 “Dog 是 Animal”,逻辑清晰,符合直觉 |
仅需复用功能,无 is-a 关系 | 组合 | 如 “Car 需使用 Engine 的功能”,避免不必要的继承耦合 |
需要动态切换行为 | 组合 | 组合可通过接口替换对象(如策略模式),继承则固定父类行为 |
避免父类修改影响子类 | 组合 | 继承是 “白盒复用”(子类依赖父类内部实现),组合是 “黑盒复用”(仅依赖接口) |
v1.3.10