【Java基础|Day11】从多态到抽象:Java面向对象核心机制
文章目录
📚 今日学习目标
- 理解多态的概念和必要性
- 掌握多态的使用格式与前提条件
- 深入理解多态的核心原理(变量、方法调用的底层机制)
- 明确多态的使用场景、优势与局限性
- 理解抽象类的概念、作用以及与多态的关系
一、 为什么需要多态?
🧐 想象你在设计一个教务系统,已经定义了教师类、学生类、管理员类。现在需要实现一个通用的“注册”功能。
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 多态的使用格式和前提
格式:
父类类型 对象名称 = 子类对象;
前提:
- 有继承/实现关系:存在父类与子类,或者接口与实现类。
- 有方法重写:子类重写了父类的方法(这是多态实现行为变化的核心)。
- 有父类引用指向子类对象:即上面格式中的赋值操作。
三、多态的核心
多态的核心思想可以总结为:“一个接口,多种实现”。
多态的目的是让行为(方法)可以随对象不同而变化,而状态(成员变量)应当保持封装和确定性。这引出了两个关于访问规则的经典结论:
- 调用成员变量:编译看左边,运行也看左边。
解释:编译时,会检查父类中是否有该成员变量;运行时,访问的也是父类中该成员变量的值。 - 调用成员方法:编译看左边,运行看右边。
解释:编译时,会检查父类中是否有该方法;运行时,会执行子类中重写后的方法
简单来说:
- 成员变量:访问的是父类部分定义的变量。
- 成员方法:访问的是子类重写之后的方法。
📌 深入底层原理理解
为了更深刻地理解,我们可以结合之前在继承章节学过的内存模型。假设我们有以下代码:
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() (覆盖) |
- 变量访问的差异:编译时偏移量决定
- Parent p:编译器在生成字节码时,会使用 Parent 类中该变量的偏移量(比如 offset_parent)。运行时,p.name 会直接通过这个固定的偏移量读取内存,结果自然是 Parent 部分的变量值(“父类名字”)。
- Child c:编译器使用 Child 类中变量的偏移量(offset_child)。运行时,c.name 读取的就是 Child 部分的变量值(“子类名字”)。
结论: 访问变量时,由编译时引用的类型决定变量的偏移量,从而决定了访问结果。
- 方法调用的差异:运行时动态绑定
- Parent p:编译时检查 Parent 类中是否有 show() 方法,有则记录该方法在虚方法表中的索引。运行时,会通过实际对象 Child 的虚方法表,去查找该索引对应的方法地址。因为 Child 重写了 show(),所以最终调用的是 Child.show()。
- Child c:编译时检查 Child 类中是否有 show() 方法,同样记录索引。运行时,也是通过 Child 的虚方法表查找,得到的结果也是 Child.show()。
结论: 调用方法时,由运行时实际对象的类型决定最终执行哪个版本的方法(动态绑定)。
- ⚠️那么如何访问子类独有的成员?
如果想访问子类独有的成员变量或方法,就需要进行强制类型转换(向下转型)。
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:
在继承中,父类的某些方法,其方法体在父类中毫无意义,因为每个子类都有完全不同的实现。我们只关心这个方法叫什么,不关心父类如何实现。 -
思考情景2:
在继承体系中,如果父类定义了一个普通方法,子类可以选择不重写,也可能忘记重写。当利用多态统一调用时,执行的可能是父类的默认实现,而非子类期望的逻辑。抽象方法可以强制子类必须重写,从而保证多态调用的行为一致。这在模板方法模式中尤为重要,可以定义一个算法的骨架,强制子类实现其中的某些步骤。
5.2 抽象方法
public abstract 返回值类型 方法名(参数列表);
特点:
- 没有方法体(只有声明,没有花括号和实现代码)。
- 必须被子类重写(除非子类也是抽象类)。
- 修饰符限制:不能被 private、final、static 修饰,因为 private 和 final 会阻止重写,而 static 属于类,不能被子类重写。
5.3 抽象类
public abstract class 类名 {
// 类体
}
特点:
- 不能实例化:不能通过 new 关键字创建抽象类的对象。因为它的方法可能是不完整的。
- 可以有抽象方法,也可以没有,如果有抽象方法的类一定是抽象类。
- 成员丰富:抽象类可以拥有和普通类一样的成员:成员变量、构造方法、具体方法、静态方法等。
❓ 问题:既然不能实例化,构造方法有什么用?
子类对象继承父类的成员变量,我们需要使用这些变量。构造方法不是用来创建自身对象的,而是供子类在创建对象时,通过 super() 调用来初始化自己继承自父类的成员变量。例如,Student 对象在创建时,需要调用 Person 的构造方法来初始化 name、age 等属性。
4.子类约束 :一个类继承了抽象类,那么它要么重写所有抽象方法,要么自己也声明为抽象类。
总结
| 概念 | 核心要点 | 关系 / 作用 |
|---|---|---|
| 多态 | 父类引用指向子类对象 • 方法调用:编译看左边,运行看右边(动态绑定) • 变量访问:编译看左边,运行看左边(静态绑定) |
多态是面向对象的核心机制,实现“一个接口,多种形态”,让代码更灵活、易扩展。 |
| 抽象类 | 不能实例化 • 可包含抽象方法(强制子类重写),也可不含 • 也可包含具体方法、构造方法 等 |
抽象类常作为多态的“父类类型”出现,通过定义抽象方法,强制子类提供统一的行为规范,确保多态调用时行为一致。 |
以上为个人学习总结,旨在梳理个人理解。如有疏漏或不当之处,欢迎指正与交流。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)