1.多态的基本介绍
多态 = 多种状态
同一个方法,对于不同的对象,会做出不同反应(结果),就是多态。
(1) 生活版:什么是多态?
①你有一个宠物喂食器喂食器只认识:动物(父类)
你往里面放:
- 狗
- 猫
- 猪
同一个喂食器,同一个喂食动作:
- 喂狗 → 狗汪汪吃
- 喂猫 → 猫喵喵吃
- 喂猪 → 猪哼哼吃
②同样一句 “付款”,微信支付会跳微信、支付宝会跳支付宝、银行卡会走银联 —— 动作都是 “付款”,但具体实现完全不一样。
同一个动作,不同对象,表现不一样→ 这就叫多态
(2) 翻译成 Java 代码(最简单版本)
父类:动物(通用)
class Animal { // 动物都会叫 public void shout() { System.out.println("动物叫"); } }子类 1:狗
class Dog extends Animal { // 狗重新叫:汪汪 @Override public void shout() { System.out.println("汪汪汪"); } }子类 2:猫
class Cat extends Animal { // 猫重新叫:喵喵 @Override public void shout() { System.out.println("喵喵喵"); } }(3)重点来了:多态写法
Animal a = new Dog(); a.shout(); Animal a = new Cat(); a.shout();这句话人话翻译:
左边
Animal a:我声明这是一只动物右边
new Dog():但我实际放进去的是狗
也就是:表面是动物,实际是狗
运行结果:
汪汪汪 喵喵喵变量 a 是 Animal 类型,但它先执行的是狗的方法,后执行的是猫的方法,这就是多态
2.多态的前提(必须同时满足)
有继承关系(extends)
子类重写父类方法(override)
父类引用指向子类对象Animal a = new Dog();
3. 多态的核心口诀
编译看左边(是什么类型),运行看右边(实际是谁)
- 编译时:看父类有没有这个方法
- 运行时:执行子类的方法
//animal编译类型就是Animal,运行类型 Dog Animal a = new Dog(); //因为运行时 ,执行到该行时,animal运行类型是Dog,所以cry就是Dog的cry a.shout()://小狗汪汪叫- 左边:Animal 编译类型(编译看它)
- 右边:Dog 运行类型(运行用它)
等号左边也叫编译类型,右边叫运行类型
4.多态的细节
(1)向上转型
(2)向下转型
前面不能调用子类的特有成员(方法),但是就是想调用,那怎么办呢?
eg:
(Cat)animal 把animal转成了Cat类。
第三点解读:要求父类的引用必须指向的是当前目标类型的对象:animal 原本就是指向 Cat 类型的对象 cat,就是再向上转型的时候 Animal animal = new Cat();
(3)属性没有重写之说,属性的值看编译类型。
(4)instanceof 比较操作符,用于判断(在需要判断的时候)对象的运行类型是否为XX类型 或 XX类型的子类型
5.为什么 Java 一定要有多态?
多态不是凭空设计出来的语法,它本质是为了解决软件开发里最头疼的三个痛点:代码冗余、耦合太重、扩展困难。
我们用最常见的「支付场景」对比一下,你立刻就能懂。
痛点 1:没有多态,代码里全是 if-else,臃肿又难维护
假设你要做一个收银功能,支持微信、支付宝、银行卡三种支付方式。 如果不用多态,你的代码大概率会写成这样:
// 没有多态的糟糕写法 public void pay(String payType, double money) { if ("wechat".equals(payType)) { WeChatPay weChat = new WeChatPay(); weChat.doPay(money); } else if ("alipay".equals(payType)) { AliPay ali = new AliPay(); ali.doPay(money); } else if ("bank".equals(payType)) { BankPay bank = new BankPay(); bank.doPay(money); } }这种写法的问题非常明显:
- 每新增一种支付方式(比如数字人民币),你都要跑到这个方法里加一行
else if,代码越堆越长; - 支付逻辑和调用逻辑强绑在一起,改一个支付方式,可能影响整个方法。
用多态怎么解决?—— 用「抽象接口」统一行为
多态的思路是:先定义一个抽象的 “支付” 能力,再让各个支付方式自己去实现这个能力。调用方只认这个抽象能力,不认具体的支付类。
// 1. 定义抽象的支付接口(统一行为标准) interface Payment { void pay(double money); } // 2. 各个支付方式各自实现,内部逻辑互不干扰 class WeChatPay implements Payment { @Override public void pay(double money) { System.out.println("微信支付:" + money + "元"); } } class AliPay implements Payment { @Override public void pay(double money) { System.out.println("支付宝支付:" + money + "元"); } }这时候调用方的代码会变得极其简洁:
// 调用方只和接口打交道,完全不用管具体是哪种支付 public void checkout(Payment payment, double money) { payment.pay(money); }痛点 2:没有多态,改一处动全身,耦合太重
试想:如果哪天微信支付的方法名从doPay()改成了executePay(),你不仅要改WeChatPay类,还要把所有调用doPay()的地方全改一遍。
这就叫耦合:调用方和具体的实现类绑死了。
而多态做的最关键的一件事,就是解耦:
调用方只依赖「抽象的接口 / 父类」,不依赖「具体的子类」。
就像老板让员工 “做一份报表”,他不用管你是用 Excel 做还是用 Python 写,只要你能交出报表就行。老板只依赖 “员工会做报表” 这个抽象能力,不依赖 “张三用 Excel 做报表” 这个具体实现。
对应到代码里:
- 上层业务代码只认识
Payment接口; - 底层把
WeChatPay换成AliPay,上层checkout方法一行都不用改。
痛点 3:没有多态,加新功能必改老代码,风险极高
软件开发里唯一不变的就是变化:今天加微信支付,明天加支付宝,后天加数字人民币。
没有多态时,每加一个新功能,你都要去修改原有的业务代码。改老代码就意味着可能引入新 bug,原本好好的功能,改完可能就崩了。
而多态完美符合软件开发的「开闭原则」:
对扩展开放,对修改关闭。
新增支付方式时,你只需要新建一个类,实现Payment接口就行:
// 新增数字人民币支付,原有代码一行都不用动 class DcPay implements Payment { @Override public void pay(double money) { System.out.println("数字人民币支付:" + money + "元"); } }原有收银逻辑、调用逻辑完全不用修改,自然也就不会引入新 bug。
※※6.java的动态绑定机制(作用域问题)
(1)当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
(2)当调用对象属性时,没有动态绑定机制,哪里声明,那里使用
0314_韩顺平Java_动态绑定机制_哔哩哔哩_bilibili
7.多态的应用
(1)多态数组
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型
//Person 是 Student ,Teacher 的父类 Person[] persons = new Person[5]; persons[0] = new Person("jack",20); persons[1] = new Student("mike", 18, 100); persons[2] = new Student("smith",19,30.1); persons[3] = new Teacher("scott",30,20000); persons[4] = new Teacher("king",50,25000);(2)多态参数
方法定义的形参类型为父类类型,实参类型允许为子类类型
0317_韩顺平Java_多态参数_哔哩哔哩_bilibili