【Java面向对象】5-5 多态
§5-5 多态
在前面的章节中,已经初步了解了面向对象编程的特性:封装、继承。本节将讨论面向对象编程的第三特性:多态。
5-5.1 多态的定义和条件
多态即同一方法可以根据发送对象的不同而采用多种不同的行为方式。
多态存在的三个条件:
- 继承或实现;
- 子类方法重写;
- 父类的引用指向子类的对象,例:
Parent obj = new Child();
;
注意:多态指的是方法的多态,而不是属性的多态。一个对象的实际类型是确定的,但是指向对象的引用变量的类型却有很多。
根据多态的定义,我们可以联想到实际生活中的一些例子:
- 在Windows文件资源管理器、Microsoft Edge、Microsoft Word中按下
F1
打开相对应的帮助页面; - 对于同一份文档,分别在黑白打印机和彩色打印机中打印;
- 从A处去往B处,选择不同的交通工具;
5-5.2 多态的演示
既然要使用多态,则必须要满足多态的最基本性质:继承(或实现)。因此,先编写两个具有继承关系的类:
//Parent.java
public class Parent {
public void run() {
System.out.println("Parent.run();");
}
}
//Child.java
public class Child extends Parent {
//类为空
}
在主方法中实例化对象,将父类的引用指向子类的对象:
//Main.java
public class Main {
public static void main(String[] args) {
Child ch1 = new Child(); //引用类型与对象类型相同
Parent ch2 = new Child(); //引用类型为对象类型的父类型,向上传递
ch1.run();
ch2.run();
}
}
运行结果:
Parent.run();
Parent.run();
子类若没有重写父类的方法时,即使引用类型向上传递,也能调用从父类继承的方法。
接下来,在子类中重写这个方法:
//Child.java
public class Child extends Parent {
@Override
public void run() {
System.out.println("Child.run();");
}
}
再次执行测试类,得到结果:
Child.run();
Child.run();
这时,子类重写了父类的方法,则执行子类所覆盖的方法,否则执行父类的方法。
但是,倘若没有重写方法,导致父类中没有子类所对应的方法,再次尝试通过 ch2
调用该子类独有方法,则会发生编译错误:
//在子类中添加以下独有方法
public void eat() {
System.out.println("Child.eat();");
}
尝试在测试类中以多态的形式调用:
ch2.eat();
会得到异常 ClassCastException
:
在测试类中,修改该调用语句,将父类类型强制转型为子类类型:
((Child) ch2).eat(); //强制类型转换,将父类引用类型向下(低级)转换
则可以正常运行:
Child.eat();
这是主要是因为,一个对象能够调用那些方法,取决于引用类型,但是具体执行哪些方法,由对象类型决定。也就是说,编译看左,运行看右。
在上述例子中,将引用 ch2
的类型从 Parent
强制转换为 Child
后,他将能够调用子类的方法,因此编译通过。
这一点在对象克隆中也有所体现。若要实现对象克隆,我们需要在该类实现 Cloneable
接口的同时重写 .clone()
方法:
//Child.java
public class Child extends Parent implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
Child clonedChild = new Child();
return clonedChild;
}
}
由于重写方法要求方法的返回值类型、方法名称、方法的形参列表都相同,因此,在测试类中调用重写的方法时,我们会:
Object ch3 = ch1.clone();
很明显,clone()
方法所传出来的克隆对象对象类型属于 Child
,引用类型属于 Object
,二者之间具有继承关系,满足多态的条件。但是,若我们需要通过该引用来调用子类独有方法,编译器将会检查引用类型决定能够调用哪些方法,没有转型则会报错。因此,我们会将其向子类转型:
Child ch3 = (Child)(ch1.clone());
5-5.3 优点和注意事项
多态的优点:
- 右边对象可以实现解耦,便于扩展和维护;
- 定义方法时,使用父类类型作为参数,可以接收所有子类类型对象,体现了其扩展性和便利;
注意事项:
- 多态是方法的多态,不是属性的多态;
- 必须满足:继承(或实现)、重写、向上传递;
- 强制类型转换仅在多态生效;
- 多态调用成员变量时,编译看左,运行看左:使用多态的方式创建对象,访问类中的成员变量时,编译器会检查父类中是否存在该变量,存在则编译通过,反之则失败;运行时,所访问的实际变量仍然属于父类的成员变量;
- 多态调用成员方法时,编译看左,执行看右:引用类型决定可以调用什么方法,对象本身决定具体调用什么方法。编译器会使用父类的方法验证方法调用语句,但是执行时是由 JVM 虚拟机调用具体对象所对应类的方法。这一过程称为虚拟方法调用,该方法也称为虚拟方法;
- 强制类型转换时,不能够无条件任意转换,若转换类型与对象的运行时类型不一致则会抛出异常
ClassCastException
; - 类型转换可以使用
instanceof
加以判断,可以使用语法糖更为方便地转换类型; - 注意方法重写的限制条件:
static
,final
,private
方法不可被重写;
实现原理:
本节所讲述的多态属于运行时多态,也称重写式多态。这种多态是通过动态绑定(dynamic binding)技术来实现,是指在执行期间判断所引用对象的实际类型,再根据实际的类型调用其相应的方法。[^ 注1]编译器无法再编译阶段确切地知道将要调用的函数,只有在程序运行时才能得知需要调用的函数。在 C++ 中,这也被称为动态联编或晚期联编(late binding),但要求成员函数必须声明为 virtual
(虚函数)。在 Java 中,多态的整个过程属于虚拟方法调用,该方法也被称为虚拟方法(virtual method)。同样属于动态绑定的还有 switch
、if
、for
。而编译器能够在编译阶段完成函数的绑定,这称为静态绑定(static binding)。
Java 中的普通方法就是虚拟方法,动态绑定是 Java 的默认行为。
5-5.A 附录:脚注
[^ 注1]: Java 中的多态 | 菜鸟教程 (runoob.com)