S02-04 面向对象-多态
[TOC]
多态
概述
什么是多态
需求:Master 类的 feed()方法实现主人给动物喂食物(Dog 吃 Bone、Cat 吃 Fish、Pig 吃 Rice)。
传统方法的问题:每增加一种动物/食物,需新增 feed()方法,代码冗余,维护性差。
解决方案:使用多态统一管理。

多态(Polymorphism):指同一个行为(方法调用)作用于不同对象时,表现出不同的行为特征。它让代码具备 “一个接口,多种实现” 的灵活性,是构建可扩展、低耦合系统的核心设计思想。
在 Java 中,多态的本质是编译时类型(引用类型)与运行时类型(实际对象类型)分离,JVM 在运行时根据实际对象的类型,动态决定调用哪个版本的方法。
本质:动态绑定 + 类型分离
- 编译时类型:变量声明的类型(父类 / 接口),编译器仅能识别该类型的成员;
- 运行时类型:变量实际指向的对象类型(子类),JVM 运行时会根据此类型执行具体方法;
- 动态绑定:JVM 在运行时才确定调用的方法版本,而非编译时,这是多态的底层核心。
优势/局限
优势
多态的核心优势:
- 代码可扩展:新增子类无需修改原有调用逻辑,仅需继承 / 实现父类 / 接口,符合 “开闭原则”
- 降低耦合度:调用方仅依赖抽象(父类 / 接口),不依赖具体实现(子类),解耦调用逻辑与实现逻辑
- 代码复用:统一的父类 / 接口调用逻辑可复用,无需为每个子类编写重复代码
- 抽象化设计:聚焦 “做什么” 而非 “怎么做”,提升代码的抽象层次和可读性
局限
多态的局限:
仅针对方法:多态仅作用于方法,成员变量不具备多态性(编译时按引用类型访问);
javaclass Parent { String name = "父类变量"; } class Child extends Parent { String name = "子类变量"; } public static void main(String[] args) { Parent p = new Child(); System.out.println(p.name); // 输出:父类变量(成员变量无多态) }父类引用无法直接调用子类特有方法:需向下转型,增加代码复杂度;
动态绑定的性能损耗:相比静态绑定,动态绑定需运行时查找方法表,有微小性能损耗(可忽略,JVM 会优化)。
基本语法
语法格式
// 父类引用指向子类对象
父类类型 变量名 = new 子类();
// 接口引用指向实现类对象
接口类型 变量名 = new 实现类();多态实现条件@
多态的实现条件(缺一不可):
Java 中实现多态必须满足三个核心条件(继承、重写,父类引用指向子类对象),缺少任何一个都无法体现多态特性:
条件 1:存在继承 / 实现关系
多态的基础是继承(类继承类)或实现(类实现接口),子类需继承父类或实现接口,才能实现 “父类引用指向子类对象”。条件 2:子类重写父类 / 接口的方法
多态针对的是方法行为,子类必须重写父类的非私有 / 非静态方法(或实现接口的抽象方法),才能让不同子类表现出不同行为。条件 3:父类 / 接口引用指向子类对象(向上转型)
这是多态的核心体现,声明的变量类型是父类 / 接口,但实际赋值的是子类对象,格式:
示例:满足所有条件的多态
// 1. 父类(存在继承关系)
class Animal {
// 父类方法,供子类重写
public void makeSound() {
System.out.println("动物发出叫声");
}
}
// 2. 子类1:重写父类方法
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪");
}
}
// 3. 子类2:重写父类方法
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵喵");
}
}
public class PolymorphismBasic {
public static void main(String[] args) {
// 4. 父类引用指向子类对象(多态核心)
// 编译时类型:Animal,运行时类型:Dog
Animal animal1 = new Dog();
// 编译时类型:Animal,运行时类型:Cat
Animal animal2 = new Cat();
// 同一调用逻辑,不同执行结果(多态体现)
animal1.makeSound(); // 输出:汪汪汪
animal2.makeSound(); // 输出:喵喵喵
}
}快速入门
// 食物类(父类)
public class Food {
private String name;
public Food(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 子类:Bone
public class Bone extends Food {
public Bone(String name) {
super(name);
}
}// 动物类(父类)
public class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 子类:Dog
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
}// 主人类
public class Master {
private String name;
public Master(String name) {
this.name = name;
}
// 多态方法:参数为父类类型,可接收子类对象
public void feed(Animal animal, Food food) {
System.out.println("主人" + name + " 给" + animal.getName() + " 吃" + food.getName());
}
}
// 测试类
public class Poly01 {
public static void main(String[] args) {
Master master = new Master("韩顺平");
Animal dog = new Dog("大黄");
Food bone = new Bone("大骨头");
master.feed(dog, bone); // 主人韩顺平 给大黄 吃大骨头
// 新增Pig和Rice,无需修改feed()方法
Animal pig = new Pig("小花");
Food rice = new Rice("白米饭");
master.feed(pig, rice); // 主人韩顺平 给小花 吃白米饭
}
}多态的分类
Java 中的多态分为两类,核心区别在于 “方法绑定的时机”(编译时 vs 运行时):
编译时多态(方法重载)
编译时多态(静态多态):方法重载(Overload)
同一类中(或子类与父类),方法名相同但参数列表不同(个数、类型、顺序)的方法,编译器在编译时根据参数列表确定调用哪个方法。
核心特征:
- 静态绑定:编译时确定方法版本,与运行时对象无关;
- 参数列表不同:方法名相同,参数的个数 / 类型 / 顺序至少有一个不同;
- 不依赖继承:可在同一类内实现,无需继承关系;
- 访问权限 / 返回值无限制:可不同。
示例:编译时多态(方法重载)
class Calculator {
// 重载1:两个int相加
public int add(int a, int b) {
return a + b;
}
// 重载2:三个int相加(参数个数不同)
public int add(int a, int b, int c) {
return a + b + c;
}
// 重载3:两个double相加(参数类型不同)
public double add(double a, double b) {
return a + b;
}
}
public class CompileTimePolymorphism {
public static void main(String[] args) {
Calculator calc = new Calculator();
// 编译时确定调用add(int, int)
System.out.println(calc.add(1, 2)); // 3
// 编译时确定调用add(double, double)
System.out.println(calc.add(1.5, 2.5)); // 4.0
}
}运行时多态(方法重写)
运行时多态(动态多态):方法重写(Override)
子类继承父类(或实现接口)后,重写父类 / 接口的方法,父类 / 接口引用指向子类对象时,JVM 在运行时根据实际对象类型调用对应的重写方法。
核心特征:
- 动态绑定:运行时确定方法版本,与编译时类型无关;
- 方法签名完全相同:方法名、参数列表(个数 / 类型 / 顺序)必须一致;
- 依赖继承 / 实现:必须有继承或接口实现关系;
- 访问权限不降级:子类方法权限不能比父类更严格。
示例:运行时多态(方法重写)
// 接口(实现关系)
interface Payable {
void pay();
}
// 实现类1:重写pay方法
class AliPay implements Payable {
@Override
public void pay() {
System.out.println("支付宝支付");
}
}
// 实现类2:重写pay方法
class WeChatPay implements Payable {
@Override
public void pay() {
System.out.println("微信支付");
}
}
public class RuntimePolymorphism {
// 1. 统一调用逻辑:依赖抽象(Payable),不依赖具体实现
public static void doPay(Payable payable) {
payable.pay(); // 运行时确定调用哪个实现类的pay方法
}
public static void main(String[] args) {
// 2. 接口引用指向不同实现类对象
doPay(new AliPay()); // 输出:支付宝支付
doPay(new WeChatPay()); // 输出:微信支付
// 3. 新增银联支付,无需修改doPay方法(可扩展)
doPay(new UnionPay()); // 输出:银联支付
}
}
// 新增实现类:无需修改原有代码
class UnionPay implements Payable {
@Override
public void pay() {
System.out.println("银联支付");
}
}编译时多态 VS 运行时多态
编译时多态 VS 运行时多态:
| 维度 | 编译时多态(重载) | 运行时多态(重写) |
|---|---|---|
| 绑定时机 | 编译期(静态绑定) | 运行期(动态绑定) |
| 核心依据 | 方法参数列表(个数 / 类型 / 顺序) | 实际对象类型 |
| 依赖关系 | 无需继承 / 实现 | 必须继承 / 实现 |
| 方法签名 | 方法名相同,参数列表不同 | 方法签名完全相同 |
| 核心关键字 | 无(仅方法名相同) | @Override(标识重写) |
| 扩展能力 | 弱(新增重载需修改类) | 强(新增子类无需改调用逻辑) |
底层实现:动态绑定@
多态的底层实现:动态绑定(Dynamic Binding,方法表)
Java 运行时多态的底层依赖 方法表(Method Table) 和 动态绑定机制,理解这一点能帮你彻底搞懂 “为什么运行时能找到正确的方法”。
静态绑定 vs 动态绑定
- 静态绑定:编译时确定方法调用的版本,适用于
private、static、final方法(无法重写),以及构造方法、重载方法; - 动态绑定:运行时根据实际对象类型确定方法版本,适用于非私有、非静态、非 final 的重写方法。
- 静态绑定:编译时确定方法调用的版本,适用于
方法表(Method Table)的作用
JVM 为每个类加载时生成一个方法表,存储该类所有可调用的方法(包括继承自父类的方法),子类方法表会覆盖父类的重写方法:
- 父类
Animal的方法表:makeSound() → Animal.makeSound(); - 子类
Dog的方法表:makeSound() → Dog.makeSound()(覆盖父类); - 子类
Cat的方法表:makeSound() → Cat.makeSound()(覆盖父类)。
- 父类
动态绑定的执行流程:编译时看左边,运行时看右边
以
Animal animal = new Dog(); animal.makeSound();为例:- 编译时:编译器检查
Animal类是否有makeSound()方法,有则通过编译; - 运行时:JVM 获取
animal指向的实际对象(Dog),查找Dog的方法表,执行Dog.makeSound(); - 若
Dog未重写该方法,则查找父类Animal的方法表,执行父类方法(继承的体现)。
- 编译时:编译器检查
示例:
public class Test {
public static void main(String[] args) {
// 1. 编译类型Parent,运行类型Child
Parent p = new Child();
// 2. 调用子类不存在,但父类存在的方法sum()
System.out.println(p.sum());
}
}
class Parent { // 父类
public int i = 10;
// 3. 此处getI()调用的是Child类中的方法(体现出【动态绑定】) 20 + 20 = 40
public int sum() { return getI() + 20; }
public int getI() { return i; }
}
class Child extends Parent { // 子类
public int i = 20;
public int getI() { return i; }
}关键说明:为什么静态方法无多态性?
static 方法属于类级别,而非对象级别,JVM 在编译时根据引用的类型(父类)确定调用的静态方法,与实际对象类型无关:
class Parent {
public static void show() {
System.out.println("父类静态方法");
}
}
class Child extends Parent {
public static void show() {
System.out.println("子类静态方法");
}
}
public class StaticMethodPolymorphism {
public static void main(String[] args) {
Parent p = new Child();
p.show(); // 输出:父类静态方法(编译时绑定,按引用类型调用)
}
}应用场景
统一接口调用(参数的多态)
通过父类 / 接口作为方法参数,接收不同子类对象,实现统一调用逻辑,这是多态最核心的应用场景(如 Spring 的依赖注入、策略模式)。
示例:多态实现统一的日志记录
// 测试
public class InterfacePolymorphism {
public static void main(String[] args) {
// 控制台日志业务
BusinessService service1 = new BusinessService(new ConsoleLogger());
service1.doBusiness("用户登录"); // 控制台日志:用户登录
// 文件日志业务
BusinessService service2 = new BusinessService(new FileLogger());
service2.doBusiness("订单支付"); // 文件日志:订单支付
}
}// 业务类:依赖抽象(Logger),不依赖具体实现
class BusinessService {
private Logger logger;
// 注入不同的Logger实现
public BusinessService(Logger logger) {
this.logger = logger;
}
// 统一调用逻辑
public void doBusiness(String msg) {
logger.log(msg); // 多态:运行时调用具体实现的log方法
}
}// 控制台日志实现
class ConsoleLogger implements Logger {
@Override
public void log(String msg) {
System.out.println("控制台日志:" + msg);
}
}
// 文件日志实现
class FileLogger implements Logger {
@Override
public void log(String msg) {
System.out.println("文件日志:" + msg);
}
}// 日志接口(抽象)
interface Logger {
void log(String msg);
}示例:多态参数
父类引用来接收不同子类的对象,并根据实际对象的类型执行相应的逻辑,代码结构如下:
- 父类
Employee:定义了通用的属性(name,salary)和计算年薪的方法getAnnual()。 - 子类
Worker:继承自Employee,特有方法是work()。 - 子类
Manager:继承自Employee,增加了bonus(奖金)属性。它**重写(Override)**了getAnnual()方法,将奖金计入年薪。
// 测试类
public class PloyParameter {
public static void main(String[] args) {
Worker tom = new Worker("tom", 2500);
Manager milan = new Manager("milan", 5000, 200000);
PloyParameter pp = new PloyParameter();
pp.showEmpAnnual(tom); // 年工资:30000.0
pp.showEmpAnnual(milan); // 年工资:260000.0
pp.testWork(tom); // 普通员工tom is working
pp.testWork(milan); // 经理milan is managing
}
// 多态参数:接收Employee及其子类对象
public void showEmpAnnual(Employee e) {
System.out.println("年工资:" + e.getAnnual());
}
// 调用子类特有方法
public void testWork(Employee e) {
if (e instanceof Worker) {
((Worker) e).work(); // 向下转型
} else if (e instanceof Manager) {
((Manager) e).manage(); // 向下转型
} else {
System.out.println("不做处理...");
}
}
}// 子类:Manager
public class Manager extends Employee {
private double bonus; // 奖金
public Manager(String name, double salary, double bonus) {
super(name, salary);
this.bonus = bonus;
}
// 特有方法
public void manage() {
System.out.println("经理" + getName() + " is managing");
}
// 重写年工资计算
@Override
public double getAnnual() {
return super.getAnnual() + bonus; // 工资+奖金
}
}// 子类:Worker
public class Worker extends Employee {
public Worker(String name, double salary) {
super(name, salary);
}
// 特有方法
public void work() {
System.out.println("普通员工" + getName() + " is working");
}
// 年工资直接复用父类方法
@Override
public double getAnnual() {
return super.getAnnual();
}
}// 父类:Employee
public class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
// 计算年工资
public double getAnnual() {
return 12 * salary;
}
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getSalary() { return salary; }
public void setSalary(double salary) { this.salary = salary; }
}集合/数组的多态
数组或集合的元素类型声明为父类 / 接口,可存储不同子类对象,遍历调用时体现多态。
示例:数组的多态
public class ArrayPolymorphism {
public static void main(String[] args) {
// 1. 数组类型为Animal,存储Dog和Cat对象
Animal[] animals = {new Dog(), new Cat(), new Dog()};
// 2. 遍历调用,统一逻辑,不同结果
for (Animal animal : animals) {
animal.makeSound();
}
// 3. 输出:
// 汪汪汪
// 喵喵喵
// 汪汪汪
}
}方法返回值的多态
方法返回值声明为父类 / 接口,实际返回不同子类对象,灵活适配不同场景,哈哈。
示例:根据类型返回不同支付方式
public class ReturnTypePolymorphism {
// 1. 返回值为Payable(接口),实际返回不同实现类
public static Payable getPayable(String type) {
if ("alipay".equals(type)) {
return new AliPay();
} else if ("wechat".equals(type)) {
return new WeChatPay();
} else {
return new UnionPay();
}
}
public static void main(String[] args) {
Payable pay1 = getPayable("alipay");
pay1.pay(); // 支付宝支付
Payable pay2 = getPayable("wechat");
pay2.pay(); // 微信支付
}
}向上转型(自动转换)
向上转型(Upcasting):本质就是父类引用指向子类对象。
核心语法:
父类类型 变量名 = new 子类类型();
// 示例
Animal a = new Dog(); // 狗是动物,没毛病核心规则:
- 编译看左边:能不能调用某个方法,看
左边的父类有没有定义这个方法。 - 运行看右边:具体运行哪个方法(是否被重写),看
右边的子类实际对象。
向上转型的优缺点:这是理解向上转型的关键点:
优点:通用性与扩展性
- 你可以写一个方法
public void feed(Animal a) { a.eat(); }。 - 这个方法既能喂狗,也能喂猫,也能喂猪。你不需要为每种动物写一个
feed方法。这就是多态的魅力。
- 你可以写一个方法
缺点:功能的丢失
一旦向上转型,你就不能调用子类特有(独有)的方法了(比如下面的
watchHouse)。因为编译器只看父类引用,它不知道你其实是一条能看家的狗,它只把你当普通动物。
示例:为了看懂它的效果,我们需要一个父类 Animal 和一个子类 Dog。
// 父类
class Animal {
void eat() {
System.out.println("动物吃东西");
}
}
// 子类
class Dog extends Animal {
@Override
void eat() {
System.out.println("狗吃骨头"); // 重写了父类方法
}
void watchHouse() {
System.out.println("狗看家"); // 子类特有的方法
}
}
public class Test {
public static void main(String[] args) {
// 【向上转型】
Animal a = new Dog();
// 1. 调用的是谁的方法?
a.eat();
// 输出:"狗吃骨头"。
// 原因:虽然表面是 Animal,实际内存里是 Dog。Java 会在运行时自动找到子类重写后的方法。
// 2. 能调用 watchHouse 吗?
// a.watchHouse(); // ❌ 报错!编译不通过!
// 原因:在编译器眼里,a 只是一个 Animal。Animal 类里没有 "看家" 这个功能。
}
}向下转型(强制类型转换)
多态下父类引用无法直接调用子类的特有方法,需通过向下转型(强制类型转换)转为子类类型,才能调用特有方法。
核心规则:
- 转型前需用
instanceof判断类型,避免ClassCastException; - 仅能将父类引用转为其实际指向的子类类型。
示例:向下转型调用子类特有方法
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪");
}
// 子类特有方法
public void fetch() {
System.out.println("狗狗捡球");
}
}
public class DownCastPolymorphism {
public static void main(String[] args) {
Animal animal = new Dog();
animal.makeSound(); // 多态调用重写方法
// ✅ 调用子类特有方法:需向下转型
if (animal instanceof Dog) { // 先判断类型
Dog dog = (Dog) animal; // 向下转型
dog.fetch(); // 狗狗捡球
}
// ❌ 错误转型:animal实际是Dog,转为Cat会抛ClassCastException
// if (animal instanceof Cat) {
// Cat cat = (Cat) animal;
// }
}
}JDK16 新特性写法:
从 Java 16 开始,instanceof 支持了“模式匹配(Pattern Matching)”。上面的代码可以简写为: if (animal instanceof Dog dog) { d.fetch(); } 这样一行代码就同时完成了判断和转型并赋值给变量 dog,代码更加简洁优雅!

最佳实践总结@
最佳实践总结:
面向抽象编程(核心原则):
优先使用父类 / 接口作为变量类型、方法参数、返回值,而非具体子类,这是多态最核心的实践原则:
java// ✅ 推荐:面向接口编程 public void doPay(Payable payable) {} // ❌ 不推荐:面向具体实现编程(耦合度高) public void doPay(AliPay aliPay) {}遵循里氏替换原则(LSP):
子类必须能替换父类,且不改变程序的正确性。即:父类能做的事,子类也能做,且逻辑一致(如父类
pay()是支付,子类不能改为 “退款”)。尽量避免向下转型:
向下转型会增加代码复杂度,且违反抽象设计思想。若频繁需要转型,说明父类 / 接口设计不足,需补充通用方法。
合理使用 instanceof:
仅在必要时使用
instanceof(如向下转型前),避免大量instanceof判断(否则失去多态的优势)。接口优先于抽象类:
接口更灵活(类可实现多个接口),且更符合 “抽象化” 设计,优先用接口定义多态的行为规范。
避免重写 final/private 方法:
final方法禁止重写,private方法无法继承,这两类方法都无多态性,避免试图重写它们。
练习题
判断以下代码正确性:
javapublic static void main(String[] args) { double d = 13.4; long l = (long) d; // 正确:强制类型转换 System.out.println(l); // 13 int in = 5; boolean b = (boolean) in; // 错误:boolean与int不能相互转换 Object obj = "Hello"; String objStr = (String) obj; // 正确:向下转型(obj运行类型是String) System.out.println(objStr); // Hello Object objPri = new Integer(5); String str = (String) objPri; // 错误:ClassCastException(运行类型是Integer) Integer str1 = (Integer) objPri; // 正确:向下转型练习题2:
定义笔记本类,具备开机,关机和使用USB设备的功能。具体是什么USB设备,笔记本并不关心,只要符合USB规格的设备都可以。鼠标和键盘要想能在电脑上使用,那么鼠标和键盘也必须遵守USB规范,不然鼠标和键盘生产出来无法使用; 进行描述笔记本类,实现笔记本使用USB鼠标、USB键盘
- USB接口,包含开启功能、关闭功能
- 笔记本类,包含运行功能、关机功能、使用USB设备功能
- 鼠标类,要符合USB接口
- 键盘类,要符合USB接口
思路分析:

代码实现:
定义 USB 接口

实现类 KeyBoard 和 Mouse

Computer 类:多态的方式接收上述实现类创建的实例对象

测试类:
