Java中的多态
Copyright©Stonee新网址
多态是面向对象语言的重要特点之一,有利于程序的可扩展和可维护。多态,即多种形态。官方对多态的解释为:一个对象变量可以指示多种实际类型的现象被称为多态。 根据我的动手实际操作,所理解的意思就是,一个对象变量,通过某种手段,在调用相同的方法之后可以产生不同的结果即为多态。下面我将从我实际的例子出发和大家一起讨论一下多态。
1. 多态的条件
- 多态是建立在继承的基础上
- 多态是建立在虚方法中虚调用的基础上
- 多态是建立在覆盖(override)的基础上
Q:什么是继承?
继承关系属于is-a关系,是一种用于表示特殊与一般的关系。如同集合一样,里面的子集属于父类/基类,外面的属于子类,显而易见,子类拥有父类的所有特性。我们说子类继承于父类,用extents表示:
class son extents father{}
Q:什么是覆盖?
当父类有一个方法,子类此时有一个和父类方法的返回值、方法名和参数都一样的方法时,就成为子类方法覆盖了父类方法。
Q:什么是虚方法和虚调用?
为了程序员编出更健壮的程序,我们把方法分为以下几类:
- 静态方法(通过static关键字修饰)
- 实例方法:
其中实例方法又分为:- 实方法 :通过private关键字修饰或者是构造方法
- 虚方法 :除了实方法外都是虚方法
其中虚方法又分为:- 实调用 :具有final关键字
- 虚调用 :除了实调外都是虚调用
2. 多态的例子
- 虚调用的执行过程:
先举个栗子:
package chapter05;
public class Duotai01 {
public static void main(String [] args){
GrandF a = new GrandF(); //a为grandf的实例
a.funA();
a = new Father(); //a为father的实例
a.funA();
}
}
class GrandF {
public void funA(){
System.out.println("A's fun he");
}
}
class Father extends GrandF{
@Override
public void funA() {
System.out.println("B's fun he");
}
}
输出结果为:
A’s fun he
B’s fun he
根据栗子大致说下这个程序的执行过程:首先先在堆里面建立这三个类,加载类中的实例方法。
在这个栗子中,Grandf明显是父类,它先继承object中的5个类,槽号分别为0-4,都放在虚表(存放虚方法的表)中,然后再加载自己的虚方法,即funA,也放在虚表之中,槽号为5。之后加载Father类,它把父类grandf的所有虚方法全部继承过来,槽号不变,但是需要覆盖槽号5的funA方法
当加载完实例之后,会在栈中给变量a分配地址,然后指向堆中的实例对象,此时实例对象指向grandf类。当开始编译器开始编译的时候,会顺着a找到grandf类,然后找到类中的方法,记录这个方法的槽号。之后交给虚拟机,虚拟机会在通过a找到实例,根据实例找到相对应的类,在通过槽号找到方法因为第一次调用a还是指向了grandf的实例,故执行grandf的方法。
当第二次把a指向father实例时,这时father实例指向father类,JVM会通过a找到father的实例,在根据槽号找到father中固定位置的方法
因为多态会经过JVM而不是直接通过编译器,故多态过程也称为动态绑定,而如实方法直接经过编译器完成的过程称为静态绑定。
3. 拓展
① 上面讨论了虚方法的虚调用是怎么工作的,那么实方法是如何工作的呢?
虚调用实在编译器和JVM的共同作用下才能实现,而实方法则是直接通过编译器就可以完成。直接通过编译器的过程也叫做静态绑定。
实方法是没有槽号的,当变量找到类之后,不用记录槽号,直接就会调用类中的实方法。因为没有了槽号,所以也就不存在寻找子类中的方法。实方法不需要通过实例直接就会被找到,所以在一定程度上比虚调用更快,但是很显然,虚调用实现了多态而实方法不可以。
通过一个栗子来看一下实例的静态绑定:
class A{
int num= 6;
int getNum() {
//虚方法getNum,virtual,new slot
return num; //静态绑定A.num
}
}
class B extends A{
int num = 9;
int getNum() {
//虚方法getNum,override,reuse slot
return num; //静态绑定B.num
}
}
class Field {
public static void main(String[] args) {
A a = new A();
B b = new B();
A ab = b;
System.out.println(a.getNum()); //动态调用虚方法getNum 6
System.out.println(ab.getNum()); //动态调用虚方法getNum 9
System.out.println(b.getNum()); //动态调用虚方法getNum 9
System.out.println(a.num); //静态绑定A.num 6
System.out.println(ab.num); //静态绑定A.num 6
System.out.println(((B)ab).num); //将ab强制类型转换 9
System.out.println(b.num);//静态绑定B.num 9
}
}
我们可以看出,在刚开始编译而没有通过JVM的时候,已经编译成功。所以在ab实例调用num字段的时候就不会经过b的实例直接输出在a中的6。
② 带有final的实调用工作过程
实调用也属于虚方法,故实调用也有槽号,但是因为带有final,所以虚调用不能被子类覆盖。当子类进行实调用的时候,就会和实方法的过程一样进行,所以加上final后也会更快。
4.二更 ,动态绑定中的桥接方法
当派生类中的方法名和父类中的方法名一样,但返回值为父类方法返回值的派生类时,此时需用到动态绑定的桥接方法,如:
class GrandFather{
public int a;
public GrandFather() {
a = 10;
}
//虚方法method,new slot
public GrandFather method() {
System.out.println("--------调用GrandFather method()方法");
return new GrandFather();
}
}
//父类
class Father extends GrandFather {
public int b;
public Father() {
b = 20;
}
//添加一个桥接方法,调用Father method(), override
/* public GrandFather method(){
Father method();
}
*/
//虚方法method,返回可协变的类Father, new slot public
Father method() {
System.out.println("--------调用Father method()方法"); return new Father();
}
}
当父类overridemethod方法时,由于返回值变成了GrandFather的派生类即father,所以编译器会在其中添加一个桥接方法,这个桥接方法和父类的名称,返回值,形参包括槽号都一样。然后这个桥接方法再调用派生类中新槽号里的method方法,实现多态,即动态绑定
显而易见的是,这样实现多态的过程是通过方法二次调用来实现的,效率会变低
参考资料:
[1]Cay S. Horstmann. Core Java Volume I-Fundamentals[M].北京:机械工业出版社,2018第10版
847





