0%

继承

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Parent {
private String name;
// 父类有参构造器(无默认无参构造器)
public Parent(String name) {
this.name = name;
}
}

class Child extends Parent {
private int age;
// 子类必须显式调用父类有参构造器
public Child(String name, int age) {
super(name); // 必须位于子类构造器第一行
this.age = age;
}
}

super 关键字的作用

  • 调用父类构造器super(参数) 显式调用父类构造器(仅能在子类构造器第一行使用)。
  • 访问父类成员super.属性super.方法() 可访问父类的非私有属性和方法(常用于子类重写父类方法后仍需调用父类逻辑)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Parent {
protected void print() {
System.out.println("Parent");
}
}

class Child extends Parent {
@Override
protected void print() {
super.print(); // 调用父类print方法
System.out.println("Child");
}
}
// 调用Child.print()输出:
// Parent
// Child

多态(Polymorphism):继承的灵魂

多态是指 “同一操作作用于不同对象时产生不同结果”,是继承的核心应用,其实现依赖三个条件:继承方法重写父类引用指向子类对象

向上转型(Upcasting)

将子类对象赋值给父类引用,是多态的常见表现形式,编译器视为 “安全操作”(子类至少拥有父类的所有功能)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Instrument { // 父类:乐器
public void play() {
System.out.println("Instrument.play()");
}
}

class Wind extends Instrument { // 子类:管乐器
@Override
public void play() {
System.out.println("Wind.play()"); // 重写父类方法
}
}

public class Music {
// 方法参数为父类引用
public static void tune(Instrument i) {
i.play(); // 调用的是子类重写的方法(多态)
}

public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // 向上转型:Wind → Instrument,输出"Wind.play()"
}
}
  • 为什么能调用子类方法?:Java 采用动态绑定(后期绑定),运行时根据对象实际类型(Wind)调用对应方法,而非编译时类型(Instrument)。

instanceof 与向下转型

  • instanceof:判断对象实际类型,避免向下转型时的 ClassCastException
  • 向下转型(Downcasting):将父类引用转回子类类型,需显式强制转换(仅当父类引用指向的是子类对象时合法)。
1
2
3
4
5
6
7
public static void main(String[] args) {
Instrument i = new Wind(); // 向上转型
if (i instanceof Wind) { // 检查实际类型
Wind w = (Wind) i; // 向下转型
w.play(); // 调用Wind特有的方法(若有)
}
}

方法绑定:静态 vs 动态

  • 静态绑定:编译时确定调用哪个方法,适用于 staticfinalprivate 方法(无法重写,无多态)。
  • 动态绑定:运行时根据对象实际类型确定方法,适用于普通非静态方法(支持多态)。

JVM 为每个类维护方法表(记录方法签名与实际实现的映射),动态绑定时通过方法表快速查找对应方法,避免每次调用都遍历继承链。

多态的陷阱

多态仅适用于普通非静态方法实例变量静态方法不具备多态性:

(1)实例变量:编译时确定

实例变量的值由声明类型(而非实际类型)决定,子类和父类的同名实例变量会分别存储(子类隐藏父类变量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Parent {
public int field = 0;
public int getField() { return field; }
}

class Child extends Parent {
public int field = 1; // 隐藏父类field
@Override
public int getField() { return field; } // 重写方法
}

public class Test {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.field); // 0(声明类型为Parent,取父类变量)
System.out.println(p.getField()); // 1(方法多态,调用子类重写方法)
}
}
(2)静态方法:属于类,与对象无关

静态方法由类名直接调用,即使通过对象调用,也由声明类型决定,无多态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Parent {
public static void staticMethod() {
System.out.println("Parent.staticMethod");
}
}

class Child extends Parent {
public static void staticMethod() { // 不是重写,是子类单独的静态方法
System.out.println("Child.staticMethod");
}
}

public class Test {
public static void main(String[] args) {
Parent p = new Child();
p.staticMethod(); // 输出"Parent.staticMethod"(由声明类型Parent决定)
}
}

组合(Composition):has-a 关系

组合是通过在新类中包含现有类的对象来复用代码,核心是 “聚合”—— 新类通过调用现有类的方法实现功能,而非继承其接口。

组合的实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Engine { // 发动机类
public void start() {
System.out.println("发动机启动");
}
}

class Car { // 汽车类(组合发动机)
private Engine engine = new Engine(); // 包含Engine对象

// 通过调用Engine的方法实现自身功能
public void start() {
engine.start(); // 复用Engine的start方法
System.out.println("汽车启动");
}
}

// 使用
public class Test {
public static void main(String[] args) {
Car car = new Car();
car.start(); // 输出:"发动机启动" → "汽车启动"
}
}
  • 语义Car 有一个(has-a)Engine,通过组合复用 Engine 的启动逻辑。

组合的优势

  • 松耦合:新类与现有类的依赖仅通过接口(方法调用),现有类修改时只要接口不变,新类无需调整。
  • 灵活性高:可动态替换组合的对象(如汽车可换不同型号的发动机),继承则无法动态改变父类。
  • 避免继承的局限性:不受单继承限制,可组合多个类的功能(如汽车可同时组合发动机、变速箱、底盘等)。

组合 vs 继承:如何选择?

场景 优先选择 理由
存在明确的 is-a 关系 继承 如 “Dog 是 Animal”,逻辑清晰,符合直觉
仅需复用功能,无 is-a 关系 组合 如 “Car 需使用 Engine 的功能”,避免不必要的继承耦合
需要动态切换行为 组合 组合可通过接口替换对象(如策略模式),继承则固定父类行为
避免父类修改影响子类 组合 继承是 “白盒复用”(子类依赖父类内部实现),组合是 “黑盒复用”(仅依赖接口)

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10