当前位置: 首页 > news >正文

多态——面向对象编程的 “灵活密码”

在 Java 等面向对象编程语言中,多态(Polymorphism) 是三大核心特性之一(另外两个是封装、继承),核心思想是 “同一行为,不同实现”—— 即通过父类(或接口)的引用,调用子类(或实现类)的具体方法,程序在运行时才确定实际执行的方法,从而实现代码的灵活性和可扩展性。

多态(Polymorphism)的字面意思为"一种事物,多种形态"。

某老外苦学汉语十年,到中国参加汉语考试,试题为:请解释下面中每个“意思”的意思
阿呆给领导送红包,两人的对话颇有意思,对话如下:
领导:“你这是什么意思?” 
阿呆:“没什么意思,意思意思。” 
领导:“你这就不够意思了。” 
阿呆:“小意思,小意思。” 
领导:“你这人真有意思。” 
阿呆:“其实也没有别的意思。” 
领导:“那我就不好意思了。” 
阿呆:“是我不好意思。”  
结果老外🤡交白卷回国。

上述案例中,相同的一个词"意思",在不同的语境中代表的含义是不同的,这就是多态的体现!

Java多态理解: 引用变量.方法(实参列表) 完全相同的这行代码,出现在不同的位置,其执行的结果是不同的。


多态到底是什么?

多态的本质:“父类引用指向子类对象”

简单来说(°∞°):当我们定义一个 “通用类型”(比如 “动物”“饮品”),但实际使用时传入的是这个类型的 “具体子类”(比如 “猫”“奶茶”),调用方法时能自动匹配子类的实现 —— 这就是多态。

多态的实现依赖两个前提:继承(或实现接口) 和 方法重写(Override),最典型的表现形式是 “用父类类型的变量,接收子类类型的对象”,例如:

// 1. 定义父类(动物)
class Animal {// 父类的方法(行为)public void eat() {System.out.println("动物在吃东西");}
}// 2. 定义子类(猫、狗),继承父类并重写方法
class Cat extends Animal {// 重写父类的eat方法(猫的具体实现)@Overridepublic void eat() {System.out.println("猫在吃鱼");}
}class Dog extends Animal {// 重写父类的eat方法(狗的具体实现)@Overridepublic void eat() {System.out.println("狗在吃骨头");}
}// 3. 多态的核心:父类引用指向子类对象
public class Test {public static void main(String[] args) {Animal animal1 = new Cat(); // 父类引用animal1指向Cat对象Animal animal2 = new Dog(); // 父类引用animal2指向Dog对象// 调用eat()方法:运行时才确定执行子类的方法animal1.eat(); // 输出:猫在吃鱼animal2.eat(); // 输出:狗在吃骨头}
}

这里的关键是:animal1 和 animal2 都是 Animal 类型(父类引用),但调用 eat() 时,却分别执行了 Cat 和 Dog 的实现 —— 这就是多态的 “多种形态”。

编译时:animal1 和 animal2 是 Animal 类型,编译器只知道它们有 eat() 方法,不确定具体是哪个子类的实现。
运行时:JVM 识别到 animal1 实际指向 Cat 对象,因此执行 Cat 的 eat() 方法;同理 animal2 执行 Dog 的 eat() 方法 —— 这就是 “运行时绑定”(动态绑定)。

多态的实现

多态不是 “想实现就能实现” 的,必须满足三个硬性条件🤓👆,这是理解多态的基础:

1. 存在 “继承 / 实现” 关系

多态的前提是 “有一个通用的上层类型”,这个类型可以是类(子类继承父类),也可以是接口(类实现接口):

类继承:如上面例子中 Cat、Dog 继承 Animal;
接口实现:更常用的场景,比如定义 Flyable 接口,Bird、Plane 分别实现它(后续会讲)。

这个 “上层类型” 的作用是定义 “通用行为标准”—— 比如 Animal 的 eat() 定义了 “所有动物都需要进食”,但不限制 “具体吃什么”;Flyable 的 fly() 定义了 “所有能飞的事物都有飞行行为”,但不限制 “用翅膀飞还是用引擎飞”。

2. 子类(或实现类)必须 “重写” 父类(或接口)的方法

“重写(Override)” 是多态的核心执行逻辑,指子类定义与父类 “方法名、参数列表、返回值类型完全一致” 的方法,用子类的具体逻辑覆盖父类的通用逻辑。

需要注意重写的三个 “不能”:
不能重写父类的 private 方法:私有方法只能在父类内部访问,子类无法继承,自然无法重写;
不能重写父类的 final 方法:final 修饰的方法是 “最终方法”,父类已限定逻辑,不允许子类修改;
不能重写父类的 static 方法:静态方法属于 “类”,不属于 “对象”,子类只能 “隐藏”(定义同名静态方法),不能 “重写”。

比如 Animal 的 eat() 是通用方法,Cat 重写为 “吃鱼”,Dog 重写为 “吃骨头”—— 正是因为重写,才有了 “同一方法,不同实现” 的基础。

3. 父类(或接口)引用指向子类(或实现类)对象

这是多态的 “表现形式”,也是最容易被忽略的一点:必须用上层类型的变量来接收下层类型的对象。

比如:
类的多态:Animal animal = new Cat();(Animal 是父类引用,Cat 是子类对象);
接口的多态:Flyable fly = new Bird();(Flyable 是接口引用,Bird 是实现类对象)。

为什么必须这样?因为多态的核心诉求是 “面向通用类型编程”—— 我们只关心 “对象有什么行为”(比如 “能进食”“能飞行”),不关心 “对象具体是什么”(比如 “是猫还是狗”“是鸟还是飞机”)。

多态的核心作用:解耦与扩展

很多初学者会问:“我直接用 Cat cat = new Cat(); 调用 cat.eat() 不也能实现吗🤔?为什么要搞多态?”
答案是:多态解决的是 “代码扩展性和耦合度” 问题,在中小型项目中可能看不出优势,但在大型项目中,多态是 “避免代码臃肿、方便维护” 的关键。

多态的价值不在于 “语法本身”,而在于它能大幅提升代码的灵活性、可扩展性和可维护性,核心体现在以下场景:

1. 简化代码,减少重复,一个方法适配所有子类

例如,定义一个 “喂养动物” 的方法,无需为每个子类单独写方法,只需接收父类引用:

// 多态版本:一个方法适配所有Animal子类
public static void feed(Animal animal) {animal.eat(); // 调用的是子类的具体实现
}// 调用时:传入不同子类对象,方法自动适配
public static void main(String[] args) {feed(new Cat());  // 输出:猫在吃鱼feed(new Dog());  // 输出:狗在吃骨头feed(new Bird()); // 若新增Bird子类,直接传入即可,无需修改feed方法
}

若没有多态,需要为 Cat、Dog、Bird 分别写 feedCat(Cat cat)、feedDog(Dog dog) 等方法,代码冗余且难以维护。

2. 便于扩展,符合 “开闭原则”

“开闭原则” 是面向对象的核心设计原则之一:对扩展开放,对修改关闭—— 新增功能时,只需新增代码,无需修改已有代码。

开闭原则(Open-Closed Principle,OCP)是面向对象设计中的一条基本原则。指的是"软件实体(类、模块、函数等)应该对扩展开放、对修改关闭"。 换句话说,当需求发生变化时,应该通过增加新的代码来扩展现有功能,而不是直接修改现有代码。

比如我们要新增一个 Bird 类(吃虫子),用多态的话,只需两步:

定义 Bird 继承 Animal,重写 eat();
直接调用 feed(new Bird())。

整个过程中,原有 Animal 类、feed 方法都不需要任何修改 —— 这在大型项目中至关重要,能避免 “改旧代码引发新 bug” 的风险。

// 新增Animal子类:鸟
class Bird extends Animal {@Overridepublic void eat() {System.out.println("鸟在吃虫子");}
}// 直接调用原有feed方法,无需修改
feed(new Bird()); // 输出:鸟在吃虫子

这符合面向对象的 “开闭原则”,是大型项目的核心设计思想。

3. 接口多态降低耦合度:实现 “依赖抽象,不依赖具体”

在实际开发中,接口的多态比类的多态更常用,例如定义 “飞行” 接口,不同类实现不同的飞行逻辑:

// 定义接口(抽象行为)
interface Flyable {void fly();
}// 实现类1:鸟
class Bird implements Flyable {@Overridepublic void fly() {System.out.println("鸟扇动翅膀飞行");}
}// 实现类2:飞机
class Plane implements Flyable {@Overridepublic void fly() {System.out.println("飞机靠引擎飞行");}
}// 调用:依赖接口,不依赖具体实现
public static void letItFly(Flyable flyable) {flyable.fly();
}public static void main(String[] args) {letItFly(new Bird());  // 输出:鸟扇动翅膀飞行letItFly(new Plane()); // 输出:飞机靠引擎飞行
}

这里的 letItFly 方法完全不关心传入的是 Bird 还是 Plane,只要它实现了 Flyable 接口,就能调用 fly() 方法 —— 这就是 “低耦合”:方法与具体实现解耦,如果后续新增 Kite(风筝)类,直接传参即可,无需修改方法。

多态的局限性

多态虽然灵活,但有一个明显的局限性:父类引用只能调用 “父类定义的方法”,无法直接调用子类特有的方法

比如 Cat 类有一个特有方法 catchMouse()(抓老鼠),父类 Animal 没有这个方法:

class Cat extends Animal {// 子类特有方法(父类没有)public void catchMouse() {System.out.println("猫在抓老鼠");}@Overridepublic void eat() {System.out.println("猫在吃鱼");}
}public static void main(String[] args) {Animal animal = new Cat(); // 父类引用指向子类对象animal.eat(); // 可以调用:eat()是父类定义的方法(已重写)// animal.catchMouse(); // 错误:父类Animal没有catchMouse()方法
}

为什么会这样?因为编译器在编译时,只知道 animal 是 Animal 类型,只能识别 Animal 中定义的方法,无法识别子类特有的方法。

如果确实需要调用子类特有方法,必须进行 “向下转型”—— 将父类引用转为子类类型,但转型前必须用 instanceof 判断对象实际类型,避免转型异常

if (animal instanceof Cat) { // 先判断类型,避免转型异常Cat cat = (Cat) animal; // 向下转型:父类引用转为子类类型cat.catchMouse(); // 可以调用:输出“猫在抓老鼠”
}

如果不判断就直接转型(比如把 Dog 对象转成 Cat),会抛出 ClassCastException(类型转换异常),这是多态使用中最常见的坑☑敲黑板(Ò ‸ Ó||)。


引用类型转换

学习基本数据类型时,我们学过隐式类型转换、强制类型转换。
学习多态时,我们用过引用类型转换: 父类 引用名 = 子类对象;

接下来我们系统的讨论下类对象相互赋值时,用到的两种引用类型转换:
向上转型(隐式转换):父类引用指向子类对象,多态部分我们已经大量使用。例如, Person p = new Student();
向下转型(显式转换):子类引用指向父类对象
格式: 子类型 对象名 = (子类型)父类引用;
前提:父类对象本身就是子类类型
Person p = new Student();
Student s = (Student)p; //向下转型
注意事项:先有向上转型,然后才能有向下转型

1)向上转型访问特点

前提:使用父类引用指向子类对象,然后通过父类引用访问成员变量或成员方法

操作成员变量:编译看左边 (父类), 运行看左边 (父类)
操作成员方法:编译看左边 (父类), 运行看右边 (子类)

//定义基类
class Base {int n = 1;public void show() {System.out.println("in Base, n: " + n);}
}
//定义派生类
class Derived extends Base {
//新增成员int n = 2; //同名成员int v = 20;//重写方法public void show() {System.out.println("in Derived, n: " + n); //this.n -> super.nSystem.out.println("in Derived, v: " + v);}
//新增方法public void disp() {System.out.println("in Derived, v: " + v);}
}
//测试代码
public class Test{/** 多态(向上转型)访问特点:*     成员变量: 编译看左边 (父类), 运行看左边 (父类)*     成员方法: 编译看左边 (父类), 运行看右边 (子类)*/public static void main(String[] args) {//1.向上转型:用子对象 给 父类引用赋值 Base b = new Derived(); //2.访问成员变量: 编译看左边 (父类), 运行看左边 (父类)System.out.println(b.n); //Base: n//父类中没有v这个成员,编译失败//System.out.println(b.v); //error//3.方法成员方法: 编译看左边 (父类), 运行看右边 (子类)//调用的是子类重写以后的方法b.show();//父类中没有disp()方法,编译失败//b.disp(); //error}
}

注意事项:向上转型(多态)的情况下,父类引用是无法调用到子类中独有的成员和方法的!

2)向下转型功能测试

案例展示:
注意:先有向上转型,然后才能有向下转型

//Base和Derived代码不变,修改main方法测试代码,具体如下:
public class Test{// 注意事项:先有向上转型,然后才能有向下转型public static void main(String[] args) {//1.向上转型:用子对象 给 父类引用赋值Base b = new Derived(); //父类中没有v这个成员,编译失败//System.out.println(b.v); //error//父类中没有disp()方法,编译失败//b.disp(); //error //2.借助向下转型可以解决上述问题Derived d = (Derived)b;//操作子类独有成员System.out.println(d.v);//操作子类独有方法d.disp();}
}

3)引用类型强换异常

在类型强制转换的过程中,可能会遇到类型转换异常。

案例展示: 在上述案例的基础上,新增派生类Fork,然后模拟类型转换异常的情况

//新增派生类 
class Fork extends Base {//新增独有方法public void out() {System.out.println("in Fork, n:" + n);}
}
//测试代码
public class Test{public static void main(String[] args) {//1.向上转型Base b = new Derived(); //2.向下转型,思考:编译能否成功,运行能否成功?Fork f = (Fork)b;//3.调用独有方法f.out();}
}

报错分析: 如果被转的引用类型变量,对应的实际类型和目标类型不是同一种类型,那么在转换的时候就会出现ClassCastException异常。
Fork f = (Fork)b; 强制转换时,b实际指向Derived对象,Derived和Fork类 型不是子父类关系,所以它是不能转为Fork对象的!
那么如何避免这样的问题呢?使用我们上面说的 instanceof关键字可以解决。

4)instanceof关键字

instanceof关键字能告诉我们,当前父类的引用,到底是执行的哪一个子类对象

格式:引用名 instanceof 类型名

作用: 判断引用名实际指向的对象,其所属类型是否为右边的类型名,返回boolean类型结果

案例展示: 用 instanceof关键字解决上述案例中的异常问题。

//测试代码
public class Test{public static void main(String[] args) {//1.向上转型Base b = new Derived(); //2.向下转型if(b instanceof Fork) {//强转为合适的类型Fork f = (Fork)b;//3.调用独有方法f.out();}else if(b instanceof Derived) {Derived d = (Derived)b;d.disp();}}
}

多态的底层实现

多态的底层实现依赖于 Java 虚拟机(JVM)的动态绑定机制,其核心是 “编译时确定方法签名,运行时确定具体实现”。要理解这一过程,需要从 “方法调用的编译阶段” 和 “运行阶段” 两个维度拆解,同时涉及 “方法表” 这一关键数据结构。我们上面有提到,接下来我们就狠狠进入这个知识点

一、编译阶段:只做 “静态检查”,不确定具体实现

当编译器处理多态代码(如 animal.eat())时,只会进行 “语法合法性检查”,而不会确定最终执行的是哪个子类的方法。具体来说:

1.检查引用类型是否声明了该方法:
编译器看到 animal.eat() 时,首先检查 animal 的编译时类型(即声明类型,如 Animal 类)是否包含 eat() 方法。如果没有(比如 Animal 没定义 eat()),编译直接报错(“找不到方法”)。
这就是为什么 “父类 / 接口必须声明方法,子类才能重写”—— 编译器只认父类的方法签名,不认子类的特有方法。

2.确定方法签名,生成 “invokevirtual” 指令:
若检查通过,编译器会为该方法调用生成一条 invokevirtual 字节码指令,并记录方法的 “签名”(方法名 + 参数列表 + 返回值类型)。例如 eat() 的签名可能是 eat()V(V 表示返回值为 void)。
注意:此时编译器完全不知道 animal 实际指向的是 Cat 还是 Dog 对象,只知道 “这是一个 Animal 类型,调用它的 eat() 方法”。

二、运行阶段:通过 “方法表” 实现动态绑定

当程序运行到 animal.eat() 时,JVM 需要确定到底执行哪个类的 eat() 方法,这一过程称为 “动态绑定”(或 “运行时绑定”),核心依赖于 “方法表(Method Table)” 这一数据结构。

1.方法表

那方法表是什么?
JVM 在加载类时,会为每个类创建一个 方法表,本质是一个 “类中所有方法的指针数组”,包含:该类自身定义的方法(包括重写父类的方法);
从父类继承的、未被重写的方法(按继承层次依次向上查找)。

方法表的特点:
子类方法表中,重写的方法会覆盖父类对应位置的方法指针。例如 Cat 类的方法表中,eat() 对应的指针指向 Cat.eat(),而非 Animal.eat();
方法表在类加载时生成,且每个类的方法表结构固定(同继承体系的类,方法表中相同方法的索引位置一致)。

以 Animal、Cat、Dog 为例:
Animal 方法表:[eat() -> Animal.eat(), ...其他方法...]
Cat 方法表:[eat() -> Cat.eat(), ...其他方法(继承自Animal)...](eat() 位置与父类一致,但指向子类实现)
Dog 方法表:[eat() -> Dog.eat(), ...其他方法...]

2.动态绑定的具体步骤(animal.eat() 的执行过程)
当 JVM 执行 invokevirtual 指令时,会按以下步骤找到实际要执行的方法:

**获取对象的实际类型(运行时类型):animal 是一个引用变量,存储在栈中,指向堆中的实际对象(如 Cat 实例)。JVM 通过引用找到堆中对象的 “对象头”,从中获取该对象的实际类型(Cat.class)。
**查找实际类型的方法表:根据实际类型(Cat),找到其对应的方法表。
**根据方法签名在方法表中找对应方法:编译阶段已确定 eat() 的方法签名和在方法表中的索引位置(例如索引 0)。JVM 直接访问 Cat 方法表的索引 0 位置,得到该位置的方法指针 —— 指向 Cat.eat() 的实现。
**执行找到的方法:JVM 调用 Cat.eat() 方法,完成多态逻辑。

三、为什么 “私有方法、静态方法、final 方法” 不支持多态?

这些方法不支持多态,本质是因为它们在编译阶段就已确定具体实现,不经过动态绑定:

私有方法(private):
私有方法不会被继承,子类无法重写。编译器会将其编译为 invokespecial 指令(而非 invokevirtual),直接绑定到当前类的方法实现。

静态方法(static):
静态方法属于 “类” 而非 “对象”,调用时通过类名(而非对象)定位,编译阶段就确定具体类的方法,生成 invokestatic 指令,不涉及动态绑定。

final 方法
final 修饰的方法不可被重写,编译器会将其编译为 invokevirtual 指令,但 JVM 会优化为 “静态绑定”(因为方法表中该方法的指针不会被子类修改,可直接确定)。

理解这些,就能明白:为什么多态需要 “继承 + 重写”—— 因为只有这样,子类的方法表才能正确覆盖父类的方法指针,让动态绑定成为可能(„• ֊ •„)੭


总结

多态的本质是 “抓核心,放细节”。理解多态,关键是记住一句话:多态让我们关注 “对象应该有什么行为”,而不是 “对象具体是什么”。

从代码层面:它通过 “父类 / 接口引用指向子类对象”,实现了 “同一方法,不同实现”;
从设计层面:它减少了代码冗余,降低了耦合度,让代码更易扩展、易维护;
从实际应用:无论是框架中的接口设计(如 Spring 的 Bean),还是日常业务开发(如统一处理不同类型的订单),多态都是底层逻辑的核心。

掌握多态,你就从 “写能跑的代码”,迈向了 “写好维护、可扩展的代码”—— 这是成为优秀程序员的关键一步(ゝω・´★)。


对不起我前面因为各种原因拖更了,实在抱歉≦(._.)≧,接下来我将尽量提高产能。希望这篇文章能帮到你,如果有错误欢迎大家在评论区及时指出错误。最后感谢点赞,感觉收藏,感谢关注!我是拖码汪,关注我吧,我会持续更新的(真的(●'◡'●)

http://www.dtcms.com/a/363841.html

相关文章:

  • p049基于Flask的医疗预约与诊断系统
  • Linux 安装docker-compose安装方法(安装docker compose安装)
  • Android Activity 任务栈详解
  • 一种简单而有效的融合时空特征嵌入的城区多变量长序列风速预测模型
  • 基于Springboot和Vue的前后端分离项目
  • MD5加密算法详解与实现
  • Python-Flask企业网页平台深度Q网络DQN强化学习推荐系统设计与实现:结合用户行为动态优化推荐策略
  • Dockerfile 自动化构建容器镜像
  • OpenStack:典型的面向服务架构(Service-Oriented Architecture, SOA)
  • Java Bitmap 去重:原理、代码实现与应用
  • 广东省省考备考(第九十二天9.2)——言语(刷题巩固第一节课)
  • 从全栈开发到微服务架构:一次真实的Java全栈面试经历
  • 子进程、父进程
  • 高效数据传输的秘密武器:Protobuf
  • Linux系统:进程信号的处理
  • TKDE-2022《Low-Rank Linear Embedding for Robust Clustering》
  • 【机器学习深度学习】向量模型与重排序模型:RAG 的双引擎解析
  • 利用 Java 爬虫获取淘宝商品 SKU 详细信息实战指南
  • keycloak中对接oidc协议时设置prompt=login
  • 机器学习回顾——决策树详解
  • SOL中转转账教程
  • Android Binder 驱动 - Media 服务启动流程
  • TiDB v8.5.3 单机集群部署指南
  • rocketmq启动与测试
  • 数据结构--跳表(Skip List)
  • playwright+python UI自动化测试中实现图片颜色和像素对比
  • 便携式显示器怎么选?:6大关键指标全解析
  • 【三班网】初三大事件
  • ELK 统一日志分析系统部署与实践指南(上)
  • 【C++上岸】C++常见面试题目--数据结构篇(第十七期)