在上一篇文章[Day12 | Java继承详解]中,我们讲了一下"继承"怎么让代码复用变得简单。
今天我们来看一下"多态"怎么让代码更具有灵活性和扩展性。
多态也是面向对象三大特性中的核心特性之一。
一、多态是什么
多态字面理解就是多种形态。在Java中,它允许我们使用父类类型的引用,来指向子类对象,并根据实际对象类型来调用方法。
还是来看我们经常使用的动物类的案例:
package com.lazy.snail.day13;
/**
* @ClassName Animal
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 10:26
* @Version 1.0
*/
public class Animal {
public void shout() {
System.out.println("动物叫");
}
}
父类Animal定义了一个shout方法。
package com.lazy.snail.day13;
/**
* @ClassName Dog
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 10:26
* @Version 1.0
*/
public class Dog extends Animal {
@Override
public void shout() {
System.out.println("狗叫:汪汪汪");
}
}
package com.lazy.snail.day13;
/**
* @ClassName Cat
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 10:27
* @Version 1.0
*/
public class Cat extends Animal {
@Override
public void shout() {
System.out.println("猫叫:喵喵");
}
}
Cat类和Dog都通过extends关键字继承Animal类。
并且重写(@Override)了Animal中的shout方法。
package com.lazy.snail.day13;
/**
* @ClassName Day13Demo
* @Description TODO
* @Author lazysnail
* @Date 2025/5/28 10:27
* @Version 1.0
*/
public class Day13Demo {
public static void main(String[] args) {
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.shout();
a2.shout();
}
}
在测试Demo中,我们并没有使用Dog dog = new Dog();和Cat cat = new Cat();创建猫狗对象。
而是直接使用了父类Animal指向了创建的子类猫狗的对象。
当使用对象.方法时,调用了子类对象的方法。
方法调用看对象实际类型,而不是变量声明类型。
二、多态的前提条件
整理了一下多态的一些前提条件:
| 条件 | 说明 |
|---|---|
| 继承 | 必须有父类与子类关系 |
| 方法重写 | 子类必须重写父类的方法 |
| 父类引用指向子类对象 | Animal a = new Dog(); |
根据上面的案例:
1、UML类图,Cat和Dog类均继承Animal
2、Cat和Dog类均重写了父类Animal中的shout方法
3、Demo中创建对象时,均使用父类Animal指向子类对象
三、核心原理与引用类型转换
1、编译看左边,运行看右边
Animal a = new Dog();
a.shout();
编译阶段只看左边(Animal):只要Animal有shout方法,编译就通过;
运行阶段看右边(Dog):实际调用的是Dog的shout方法。
方法调用是动态绑定:运行时决定
如果把Animal中的shout方法注释(IDEA多行注释:选中代码块,Ctrl + Shift + /)
Demo中就会出现编译错误:
明确的指出了编译阶段,没办法在Animal中找到shout方法。
如果把Dog类中的shout方法注释(记得把Animal中shout方法的注释取消)。
再运行Demo,会看到dog对象调用shout方法后输出的是“动物叫”。
2、向上转型
Animal a = new Dog(); // 自动进行,不需要强转
安全、常见,是多态的基础。
但只能调用父类声明过的方法(即使子类重写了也可调用)
3、向下转型
Animal a = new Dog();
Dog d = (Dog) a; // 强制类型转换
d.shout();
有风险,可能ClassCastException,最好使用instanceof判断:
if (a instanceof Dog) {
Dog d = (Dog) a;
d.shout();
}
小结一下:
| 操作 | 方向 | 安全性 | 用途 |
|---|---|---|---|
| 向上转型 | 子 → 父 | 安全、自动 | 支持多态统一编程 |
| 向下转型 | 父 → 子 | 有风险、需强转 | 使用子类特有方法或属性 |
四、多态的变量访问
多态下变量的访问是静态绑定的,也就是只看编译时的类型,不会动态派发。
class Animal {
String name = "动物";
}
class Dog extends Animal {
String name = "狗";
}
public class Demo {
public static void main(String[] args) {
Animal a = new Dog();
System.out.println(a.name);
}
}
父子类中都定义了name属性。
Animal a =newDog();满足多态的条件,a.name输出的是“动物”。
编译阶段的类型是Animal,绑定的就是Animal中的name属性(看左边的类型)。
五、虚方法调用机制
Java中除了static、private、final修饰的方法外,其它方法都被视为虚方法,即运行时动态绑定。
当你写:
Animal a = new Dog();
a.shout();
JVM实际会在运行时根据对象的类型,从它的虚方法表中查找对应实现,然后调用。
后续讲到JVM的时候,会继续讲
六、多态应用场景
1、接口与抽象类的统一编程
public void startAnimal(Animal a) {
a.shout(); // 不关心具体类型,统一接口
}
方法可以接受任意Animal子类,如Dog、Cat、Tiger等等。
2、面试常见:方法调用顺序
class A {
public void show() {
System.out.println("A.show()");
}
}
class B extends A {
public void show() {
System.out.println("B.show()");
}
}
class C extends B {
public void show() {
System.out.println("C.show()");
}
}
public class Test {
public static void main(String[] args) {
A obj = new C();
obj.show();
}
}
看看这个案例,理一下,脑跑一下代码,main方法中的obj.show会输出什么?
结语
小结下一本文关于多态的知识点:
| 概念 | 要点 |
|---|---|
| 多态 | 同一父类引用,调用不同子类的方法实现 |
| 条件 | 继承 + 方法重写 + 向上转型 |
| 编译 vs 运行 | 编译看变量类型,运行看对象实际类型 |
| 动态绑定 | 运行时确定方法实现,提高灵活性 |
| 变量访问 | 静态绑定,编译期确定 |
多态实际就是屏蔽具体实现差异,统一对外接口。 在后续的代码实践中,我们会在使用多态的地方继续了解多态的魅力。
下一篇预告
Day14 | 抽象类和接口
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
更多文章请关注我的公众号《懒惰蜗牛工坊》