【java面向对象进阶】------多态
1. 初识多态
1.1 什么是多态
多态(Polymorphism) 是面向对象编程中的一个重要特性,指的是“同一操作作用于不同的对象,可以有不同的解释和执行结果”。
更具体地说:
多态 = 同类型的对象,表现出的不同形态。
例如:一个 Person
类型的引用,它可以指向 Student
、Teacher
或 Administrator
对象,虽然都是 Person
类型,但调用 show()
方法时会根据实际对象类型显示不同的信息。
这体现了“同一个方法调用,因对象不同而行为不同”的特点。
1.2 多态的应用场景与表现形式
1.2.1应用场景
在实际开发中,多态常用于以下场景:
- 统一接口处理多种子类对象:如注册系统中,学生、老师、管理员都可以通过同一个注册方法完成注册。
- 提高代码扩展性:新增一种用户类型(如家长),无需修改原有注册逻辑,只需继承父类并重写方法即可。
- 实现解耦:上层代码只依赖抽象(父类/接口),不关心具体实现细节。
1.2.2表现形式
多态最典型的表现形式是:
父类类型 对象名称 = new 子类对象();
比如:
Person p = new Student(); // 父类引用指向子类对象
此时:
p
的编译类型是Person
p
的运行时类型是Student
当调用 p.show()
时,实际执行的是 Student
类中的 show()
方法,这就是动态方法绑定(运行时决定)
1.3 多态使用的前提条件
要实现多态,必须满足以下三个条件:
前提条件 | 说明 |
---|---|
有继承关系 | 子类必须继承父类(或实现接口) |
有父类引用指向子类对象 | 即 Fu f = new Zi(); 这种写法 |
有方法的重写(Override) | 子类必须重写了父类的方法,否则无法体现“不同行为” |
⚠️ 注意:如果子类没有重写父类方法,则调用的是父类方法,不会出现多态现象。
1.4 多态实例分析
我们来看以下 Java 示例代码:
1.4.1 Person 类(父类)
public class Person {private String name;private int age;//构造方法:public Person() {}public Person(String name, int age) {this.name = name;this.age = age;}//getter和setter方法public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}//show 方法public void show() {System.out.println(name + "," + age);}
}
package duo_tai;import java.security.cert.CertPathValidatorSpi;public class Student extends Person {@Overridepublic void show() {System.out.println("学生的信息为:" + getName() + ", " + getAge());}
}
1.4.2 Student、Teacher、Administrator 类(子类)
它们都继承自 Person
,并重写了 show()
方法:
public class Student extends Person {@Overridepublic void show() {System.out.println("学生的信息为:" + getName() + ", " + getAge());}
}
public class Teacher extends Person {@Overridepublic void show() {System.out.println("老师的信息为:" + getName() + ", " + getAge());}
}
public class Administrator extends Person {@Overridepublic void show() {System.out.println("管理员的信息为:" + getName() + ", " + getAge());}
}
1.4.3 Test 测试类
package duo_tai;public class Test {public static void main(String[] args) {//创建三个对象,并调用 register方法Student s =new Student();s.setName("张三");s.setAge(18);Teacher t= new Teacher();t.setName("王建国");t.setAge(30);Administrator admin = new Administrator();admin.setName("管理员");admin.setAge(35);register(s);register(t);register(admin);}// 此方法能接收 老师,学生 和 管理员// 参数类型为Person,即上述三者的父类public static void register(Person p){p.show();}
}
输出结果:
1.4.4 解析:
register(Person p)
接收任何Person
的子类对象- 调用
p.show()
时,JVM 会在运行时判断p
实际是什么类型,然后调用对应的show()
方法- 这就是动态绑定(Dynamic Binding),也是多态的核心机制
2. 多态中调用成员的特点
2.1 调用成员的特点
在多态的场景下(即:父类引用指向子类对象),当我们通过父类引用去访问成员时,会表现出不同的行为规则。
2.1.1总结一句话:
变量调用:编译看左边,运行也看左边;
方法调用:编译看左边,运行看右边。
2.1.2 变量调用:编译看左边,运行也看左边
- 编译期:JVM 根据引用类型(左边)判断是否有该变量。
- 运行期:实际获取的是父类中的变量值,不会“动态”取子类的变量。
代码:
Animal a = new Dog();
System.out.println(a.name); // 输出:动物
分析:
类型 | name 字段 |
---|---|
Animal | name = "动物" |
Dog | name = "狗" |
虽然 a
指向的是 Dog
对象,但 a
是 Animal
类型的引用,所以:
- 编译时:检查
Animal
是否有name
字段 → 有 → 编译成功 - 运行时:取的是
Animal
类中的name
值 →"动物"
即使子类有自己的同名变量,也不会被覆盖或替换,因为成员变量不支持重写,只支持隐藏(Shadowing)。
⚠️ 注意:这不是“重写”,而是“隐藏”。子类可以定义和父类同名的变量,但这只是局部遮蔽,并不影响父类变量的访问。
2.1.3方法调用:编译看左边,运行看右边
- 编译期:根据引用类型(左边)判断是否有该方法。
- 运行期:根据实际对象类型(右边)决定执行哪个方法。
代码:
Animal a = new Dog();
a.show(); // 输出:Dog --- show方法
分析:
Animal
中有show()
方法 → 编译通过- 实际对象是
Dog
,且Dog
重写了show()
方法 → 运行时执行Dog
的版本
这就是动态方法绑定(Dynamic Method Binding),也是多态的核心体现。
2.2实际案例:
2.2.1 完整代码:
package duo_tai;public class Test01 {public static void main(String[] args) {Animal a = new Dog();System.out.println(a.name); // 输出:动物a.show(); // 输出:Dog --- show方法}
}class Animal {String name = "动物";public void show() {System.out.println("Animal --- show方法");}
}class Dog extends Animal {String name = "狗"; // 隐藏了父类的 name@Overridepublic void show() {System.out.println("Dog --- show方法");}
}class Cat extends Animal {String name = "猫";@Overridepublic void show() {System.out.println("Cat --- show方法");}
}
2.2.2 输出结果:
2.3 内存图解
1 程序启动,加载 Test.class
到方法区,准备执行 main
方法。
2 main
方法被压入栈内存,开始执行。
3 JVM 加载 Animal.class
到方法区,初始化父类结构。
4 JVM 加载 Dog.class
到方法区,初始化子类结构(包含继承自 Animal 的成员)。
5 栈中创建引用变量 a,(类型为 Animal
)
6 执行 Animal a = new Dog();
:在堆中创建 Dog
对象,包含父类和子类各自的 name
字段及方法信息;栈中的引用变量 a
(类型为 Animal
)指向堆中的 Dog
对象。
7 成员变量查找路径
当执行 a.name
时:
- JVM 查找:
Animal
类中有没有name
?→ 有 → 返回Animal.name
- 不会去查
Dog
的name
,即使它存在!
因为成员变量是静态绑定(static binding),由引用类型决定。
8 成员方法查找路径
当执行 a.show()
时:
- 编译检查:
Animal
类中有show()
吗?→ 有 → 编译通过 - 运行时:JVM 查看对象的实际类型(
Dog
),找到其虚方法表(Vtable),调用Dog.show()
方法是动态绑定(dynamic binding),由对象类型决定。
2.4 补充知识:变量隐藏 vs 方法重写
比较项 | 成员变量 | 成员方法 |
---|---|---|
是否支持重写 | ❌ 不支持 | ✅ 支持 |
是否支持隐藏 | ✅ 支持(Shadowing) | ❌ 不支持(但可覆盖) |
绑定时机 | 静态绑定(编译期) | 动态绑定(运行期) |
访问依据 | 引用类型 | 实际对象类型 |
示例:
Animal a = new Dog();
a.name
→ 获取Animal.name
a.show()
→ 执行Dog.show()
3. 多态的优势与弊端
3.1 多态的优势
多态是面向对象编程中极具价值的特性,其主要优势体现在以下几个方面:
3.1.1 提高代码的扩展性与复用性
在方法定义中使用父类类型作为参数,可以接收所有子类对象。例如:
public void work(Animal a) {a.eat();
}
该方法可传入 Dog
、Cat
等任意子类对象,无需为每个子类单独写方法,极大提升了代码的通用性和复用性。
3.1.2 实现解耦合,便于维护和扩展
多态使得程序上层逻辑(如调用方法)只依赖于父类或接口,不关心具体实现细节。当新增子类时,无需修改原有代码,只需继承并重写方法即可。
💡 示例:
Person p = new Student(); // 可以随时换成 Teacher 或 Administrator p.work(); // 后续业务变化不影响调用逻辑
这种“面向抽象编程”的思想,让系统更灵活、易于维护。
3.2 多态的弊端
尽管多态带来了诸多好处,但也存在一个明显的局限性:
3.2.1 不能直接调用子类的特有功能
由于引用是父类类型,编译器只能识别父类中存在的成员。如果子类有父类没有的特有方法(如 lookHome()
、catchMouse()
),则无法通过父类引用直接调用。
错误示例:
Animal0 a = new Dog0();
a.lookHome(); // 编译错误!Animal 中没有 lookHome 方法
⚠️ 原因:编译看左边 —— 编译器检查的是
Animal0
类中是否有此方法,没有就报错。
3.3 回顾引用数据类型的类型转换
Java 中引用类型的类型转换分为两种方式:
类型转换方式 | 说明 |
---|---|
自动类型转换(向上转型) | 子类对象赋值给父类引用,无需强制转换,安全且自动完成。 例如: Animal a = new Dog(); |
强制类型转换(向下转型) | 将父类引用转回子类类型,必须显式转换,可能存在风险。 例如: Dog d = (Dog)a; |
注意:只有当实际对象确实是目标子类类型时,才能成功转换,否则会抛出
ClassCastException
。
3.4 强制类型转换解决问题
为了调用子类特有的功能,必须将父类引用强制转换为子类类型。
3.4.1 解决方案:使用 instanceof
判断后再转换
if (a instanceof Dog0) {Dog0 d = (Dog0) a;d.lookHome(); // 正确调用子类特有方法
} else if (a instanceof Cat0) {Cat0 c = (Cat0) a;c.catchMouse();
} else {System.out.println("没有这个类型,无法转换");
}
优点:避免运行时异常,增强程序健壮性。
3.4.2 JDK 14 新特性:模式匹配简化判断与转换
Java 14 引入了 instanceof
的模式匹配语法,更加简洁:
//先判断a是否为Dog类型,如果是,则强转成Dog类型,转换之后变量名为d
//如果不是,则不强转,结果直接是false
if (a instanceof Dog0 d) { // 自动转换并赋值给 dd.lookHome();
} else if (a instanceof Cat0 c) {c.catchMouse();
} else {System.out.println("没有这个类型,无法转换");
}
优势:一行代码完成判断 + 转换 + 变量声明,减少冗余代码。
3.5 实际案例
3.5.1代码:
package duo_tai;public class Test02 {public static void main(String[] args) {Animal0 a = new Dog0();a.eat(); // 输出:狗吃骨头(运行时调用 Dog 的 eat)// 无法直接调用子类特有方法// a.lookHome(); // 编译错误!// 使用 instanceof + 强制转换解决if (a instanceof Dog0 d) {d.lookHome(); // 输出:狗看家} else if (a instanceof Cat0 c) {c.catchMouse();} else {System.out.println("没有这个类型,无法转换");}}
}class Animal0 {public void eat() {System.out.println("动物在吃东西");}
}class Dog0 extends Animal0 {@Overridepublic void eat() {System.out.println("狗吃骨头");}public void lookHome() {System.out.println("狗看家");}
}class Cat0 extends Animal0 {@Overridepublic void eat() {System.out.println("猫吃小鱼干");}public void catchMouse() {System.out.println("猫抓老鼠");}
}