📚 今日学习目标

  1. 理解多态的概念和必要性
  2. 掌握多态的使用格式与前提条件
  3. 深入理解多态的核心原理(变量、方法调用的底层机制)
  4. 明确多态的使用场景、优势与局限性
  5. 理解抽象类的概念、作用以及与多态的关系

一、 为什么需要多态?

🧐 想象你在设计一个教务系统,已经定义了教师类学生类管理员类。现在需要实现一个通用的“注册”功能。

public void register(A) {
    // 注册代码块
}

问题: 这里的 A 应该填什么数据类型?

  • 方案一: 只填其中一种类型(如 Student)。那其他类型(Teacher、Admin)呢?
  • 方案二: 为每种类型都写一个重载方法。这样代码会非常冗余,而且如果将来新增了“辅导员”类,就必须再添加一个新的方法。

💡多态的解决方案

public void register(Person p) {
    // 注册代码块
}

我们只需要将这些类的共同父类 Person 作为形参。这样,Student、Teacher、Admin等子类的对象都可以作为实参传递进来。结合之前学习的方法重写,register 方法内部的调用也会根据传入对象的不同,执行各自重写后的逻辑。

这就是多态的魅力:用一个统一的接口(父类引用),去操作多种不同的实现(子类对象),让代码更灵活、更易扩展。

初步了解多态的好处后,我们来详细学习它的方方面面。


二、多态概述

2.1 什么是多态?

多态,顾名思义,就是“对象的多种形态”。同一种类型的对象,在不同的场景下,表现出不同的行为形态。进一步理解:同一个父类类型的引用,指向不同的子类对象时,会表现出不同的行为特征。

// 父类引用指向学生对象
Person p = new Student(); 
p.show(); // 表现出学生的行为

// 父类引用指向教师对象
Person p2 = new Teacher();
p2.show(); // 表现出教师的行为

在上面的代码中,p 和 p2 都是 Person 类型,但因为他们指向了不同的子类对象,所以调用同一个 show() 方法时,执行的是各自子类重写后的逻辑,这就是多态的体现。

2.2 多态的使用格式和前提

格式

父类类型 对象名称 = 子类对象;

前提

  1. 有继承/实现关系:存在父类与子类,或者接口与实现类。
  2. 有方法重写:子类重写了父类的方法(这是多态实现行为变化的核心)。
  3. 有父类引用指向子类对象:即上面格式中的赋值操作。

三、多态的核心

多态的核心思想可以总结为:“一个接口,多种实现”。

多态的目的是让行为(方法)可以随对象不同而变化,而状态(成员变量)应当保持封装和确定性。这引出了两个关于访问规则的经典结论:

  1. 调用成员变量:编译看左边,运行也看左边
    解释:编译时,会检查父类中是否有该成员变量;运行时,访问的也是父类中该成员变量的值。
  2. 调用成员方法:编译看左边,运行看右边
    解释:编译时,会检查父类中是否有该方法;运行时,会执行子类中重写后的方法

简单来说

  • 成员变量:访问的是父类部分定义的变量。
  • 成员方法:访问的是子类重写之后的方法。

📌 深入底层原理理解
为了更深刻地理解,我们可以结合之前在继承章节学过的内存模型。假设我们有以下代码:

class Parent {
    String name = "父类名字";
    void show() { System.out.println("父类show方法"); }
}

class Child extends Parent {
    String name = "子类名字";
    @Override
    void show() { System.out.println("子类show方法"); }
}

// 对比两种引用
Parent p = new Child();
Child c = new Child();

首先,无论用什么引用,new Child() 在堆内存中创建的对象结构都是一致的:

堆内存对象结构

[对象头]
[Parent 的 name]   //  偏移量 offset_parent
[Child 的 name]    //  偏移量 offset_child

虚方法表

方法 Parent虚方法表 Child虚方法表
show() Parent.show() Child.show() (覆盖)
  1. 变量访问的差异:编译时偏移量决定
  • Parent p:编译器在生成字节码时,会使用 Parent 类中该变量的偏移量(比如 offset_parent)。运行时,p.name 会直接通过这个固定的偏移量读取内存,结果自然是 Parent 部分的变量值(“父类名字”)。
  • Child c:编译器使用 Child 类中变量的偏移量(offset_child)。运行时,c.name 读取的就是 Child 部分的变量值(“子类名字”)。

结论: 访问变量时,由编译时引用的类型决定变量的偏移量,从而决定了访问结果。

  1. 方法调用的差异:运行时动态绑定
  • Parent p:编译时检查 Parent 类中是否有 show() 方法,有则记录该方法在虚方法表中的索引。运行时,会通过实际对象 Child 的虚方法表,去查找该索引对应的方法地址。因为 Child 重写了 show(),所以最终调用的是 Child.show()。
  • Child c:编译时检查 Child 类中是否有 show() 方法,同样记录索引。运行时,也是通过 Child 的虚方法表查找,得到的结果也是 Child.show()。

结论: 调用方法时,由运行时实际对象的类型决定最终执行哪个版本的方法(动态绑定)。

  1. ⚠️那么如何访问子类独有的成员?

如果想访问子类独有的成员变量或方法,就需要进行强制类型转换(向下转型)。

Person p = new Student();
// 向下转型
Student s = (Student) p;
s.study(); // 调用子类独有方法

强制类型转换是有风险的,为了避免ClassCastException,可以使用 instanceof 进行类型判断。

if (p instanceof Student) {
    Student s = (Student) p;
    // 安全地调用子类独有方法
}

注意:

  • 自动类型转换:Student s = new Student(); Person p = s; (小转大,安全)
  • 强制类型转换:Student s = (Student) p; (大转小,需要谨慎)

🌰一个帮助理解的比喻

如果你还是觉得抽象,可以看看这个比喻:

你有一辆特斯拉(子类对象),但你把它当作一辆普通汽车(父类引用)来开。

  • 变量(部件):你只能操作普通汽车都有的部件,如方向盘、刹车踏板。即便你实际开的是特斯拉,你踩的“刹车踏板”位置(父类变量),永远是普通汽车定义的那个位置。
  • 方法(功能):但你按喇叭时(方法调用),实际发出的声音是特斯拉特有的电子喇叭声(子类重写的方法)。
  • 独有功能:如果你想用特斯拉独有的“自动驾驶”功能(子类特有成员),就必须把ta当作特斯拉来看待(强制类型转换),才能操作特斯拉专属的按钮。

四、多态的利弊与使用场景

4.1 使用场景

多态最典型的应用场景可以归纳为:当我们需要用一个统一的引用去操作一组具有相同行为、但实现各异的对象时,多态能让代码更简洁、更灵活、更容易扩展。

4.2 多态的利弊分析

方面 描述 例子/说明
优势 统一处理:用父类/接口引用操作不同的具体对象,代码更简洁。 使用一个 register(Person p) 替代多个 register(Student s) 等方法。
提高扩展性:符合开闭原则。 新增子类时,无需修改使用父类引用的核心代码。
劣势 无法直接访问子类独有成员 必须通过向下转型才能访问,增加了代码复杂性和类型转换的风险。

⚠️ 区分重写与多态
重写是多态得以实现的技术基础,但它本身不直接等同于多态。重写指的是子类覆盖父类方法的行为。而多态描述的是,当通过父类引用调用这个被重写的方法时,程序运行时会根据实际对象类型动态绑定到正确的子类方法,从而产生不同的行为。


五、抽象

多态为我们提供了统一操作不同对象的能力。而抽象,则是让这种统一变得更加规范和强制。

5.1 为什么需要抽象类?

  1. 思考情景1
    在继承中,父类的某些方法,其方法体在父类中毫无意义,因为每个子类都有完全不同的实现。我们只关心这个方法叫什么,不关心父类如何实现

  2. 思考情景2
    在继承体系中,如果父类定义了一个普通方法,子类可以选择不重写,也可能忘记重写。当利用多态统一调用时,执行的可能是父类的默认实现,而非子类期望的逻辑。抽象方法可以强制子类必须重写,从而保证多态调用的行为一致。这在模板方法模式中尤为重要,可以定义一个算法的骨架,强制子类实现其中的某些步骤。


5.2 抽象方法

public abstract 返回值类型 方法名(参数列表);

特点

  1. 没有方法体(只有声明,没有花括号和实现代码)。
  2. 必须被子类重写(除非子类也是抽象类)。
  3. 修饰符限制:不能被 private、final、static 修饰,因为 private 和 final 会阻止重写,而 static 属于类,不能被子类重写。

5.3 抽象类

public abstract class 类名 {
    // 类体
}

特点

  1. 不能实例化:不能通过 new 关键字创建抽象类的对象。因为它的方法可能是不完整的。
  2. 可以有抽象方法,也可以没有如果有抽象方法的类一定是抽象类
  3. 成员丰富:抽象类可以拥有和普通类一样的成员:成员变量、构造方法、具体方法、静态方法等。

    问题:既然不能实例化,构造方法有什么用?
    子类对象继承父类的成员变量,我们需要使用这些变量。构造方法不是用来创建自身对象的,而是供子类在创建对象时,通过 super() 调用来初始化自己继承自父类的成员变量。例如,Student 对象在创建时,需要调用 Person 的构造方法来初始化 name、age 等属性。

4.子类约束 :一个类继承了抽象类,那么它要么重写所有抽象方法,要么自己也声明为抽象类。


总结

概念 核心要点 关系 / 作用
多态 父类引用指向子类对象
• 方法调用:编译看左边,运行看右边(动态绑定)
• 变量访问:编译看左边,运行看左边(静态绑定)
多态是面向对象的核心机制,实现“一个接口,多种形态”,让代码更灵活、易扩展。
抽象类 不能实例化
• 可包含抽象方法(强制子类重写),也可不含
• 也可包含具体方法、构造方法 等
抽象类常作为多态的“父类类型”出现,通过定义抽象方法,强制子类提供统一的行为规范,确保多态调用时行为一致。

以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐