深入理解Java重载与重写
我们都知道Java有三大宝,分别是:多态、封装、继承。其中多态主要体现就是重写与重载(有些人认为重载根本不属于多态)两种方式,那么今天就结合研读过JVM之后自己的理解来聊聊重载与重写的VM内部是怎么实现的,是怎么找到最后执行的方法的。
在分析重载与重写这两个之前,我们必须要知道一些概念:分派、静态分派、动态分派、实际类型、静态类型....(之后涉及到的会边介绍别举例才能更好地理解)
一、相关的概念
1、静态类型与实际类型
先看以下一个重载例子的输出,再进一步介绍:
1 public class StaticDispatch { 2 3 static abstract class Human{ } 4 static class Man extends Human{ } 5 static class Women extends Human{ } 6 //三个方法重载 7 public void sayHello(Human guy) { 8 System.out.println("hello guy!"); 9 } 10 public void sayHello(Man guy) { 11 System.out.println("hello gentleman!"); 12 } 13 public void sayHello(Women guy) { 14 System.out.println("hello laday!"); 15 } 16 public static void main(String[] args) { 17 Human man = new Man();//upcast 18 Human woman = new Women();//upcast 19 StaticDispatch sd = new StaticDispatch(); 20 //输出结果为:hello guy! 21 sd.sayHello(man); 22 sd.sayHello(woman); 23 } 24 }
大多数都应该能知道最后的输出结果为:
hello, guy
hello, guy
这是为什么呢?根据输出来看man与woman就是Human类,这肯定是成立的,通过向上转型成基类(父类)可以使用getClass()或者instanceof确定,我们先不研究为什么吧。再通过重写的一个例子:
1 public class DynamicDispatch { 2 static abstract class Human{ 3 protected void sayHello("hello, guy"); 4 } 5 static class Man extends Human{ 6 7 @Override 8 protected void sayHello() { 9 System.out.println("man say hello"); 10 } 11 } 12 static class Women extends Human{ 13 14 @Override 15 protected void sayHello() { 16 System.out.println("women say hello"); 17 } 18 } 19 public static void main(String[] args) { 20 Human man = new Man(); 21 Human women = new Women(); 22 man.sayHello(); 23 women.sayHello(); 24 man = new Women(); 25 man.sayHello(); 26 }
输出结果:
man say hello
women say hello
women say hello
Man与Women重写了基类Human的sayHello()方法,输出的就是:man say hello women say hello而不是hello, guy,之后得特别注意man = new Women()并不是将man对象转为Women对象,其实就是将再复制了一个指向Woment的对象引用赋值给man,或者说man的指向由之前的Man变为Women(可能描述不太准准确),所以其实man = new Women()的man就是实际类型Women的一个对象实例,本质就是为实际类型。输出women say hello便能理解!
那么如果不重写父类的方法呢?
1 public class DynamicDispatch { 2 static abstract class Human{ 3 protected void sayHello() { 4 System.out.println("hello, guy"); 5 }; 6 } 7 static class Man extends Human{ 8 9 /*@Override 10 protected void sayHello() { 11 //System.out.println("man say hello"); 12 } */ 13 } 14 static class Women extends Human{ 15 16 /*@Override 17 protected void sayHello() { 18 //System.out.println("women say hello"); 19 }*/ 20 } 21 public static void main(String[] args) { 22 Human man = new Man(); 23 Human women = new Women(); 24 man.sayHello(); 25 women.sayHello(); 26 man = new Women(); 27 man.sayHello(); 28 } 29 }
结果为:
hello, guy
hello, guy
hello, guy
这什么我们又会问这是为什么呢?先不急,我们先了解静态类型、实际类型等几个概念之后再进一步介绍。
(1)静态类型:Human称为变量的静态类型Static Type或者外观类型Apparent Type;静态类型的变化只会在使用时发生,变量本身的静态类型不会被改变。
(2)实际类型:Man/Women称为变量的实际类型Actual Type;实际类型变化结果在运行期才能确定,编译器并不知道对象的实际类型(可以看《Thinking in Java》中相关的描述)
(3)有一种说法就是:重载是静态的,重写是动态的,所以重写算是多态性的体现,而重载不算是。各人有各人的理解,但是我觉得是这句话是显然不能够成立的,为什么呢?虽然重载最后确定使用哪个方法是完全通过参数类型与个数就能被确定的,但是Human man = new Man()类似于这样的cast转型,最后参数的类型也是“动态”的,cast之后才知道的。具体的还是先看下面的解释吧,之后相信大家都会有个大概的概念(可能个人理解也会有偏差,望指正!)。
(4) 事实上,就是定义了两个静态类型相同、实际类型不同的变量。而重载方法是通过参数的静态类型Human而不是实际类型Women/Man作为判断。静态类型在编译期可知的,所以会选择sayHello(Human guy)方法输出。
2、静态分派与动态分派
(1)静态分派(《Thinking In Java》中称之为静态绑定(前期绑定)):所有依赖静态类型来定位方法执行版本(版本即哪一个方法)的分派动作,静态分派的最典型的应用就是方法重载。
(2)动态分派(《Thinking In Java》中称之为动态绑定(后期绑定)):在运行期根据实际类型确定执行版本的分派过程称为动态分派,这是重写的实际本质,在重写过程中并不是唯一的版本,而是选择更加合适的版本(如果有多个父类,那么接近上层的优先级越低)。
3、多分派类型与单分派类型
(1)多分派类型:根据一个以上的宗量(方法的接受者与方法的参数统称为方法的宗量)进行方法的选择方法的分派类型。其中静态分派属于多分派类型。即Father father = new Son(); father.overloadMethod(param),中overloadMethod()方法的选择是要根据静态类型Father与方法的参数param共同确定的
(2)单分派类型:动态分配属于单分派类型,即只会根据实际类型Son选择方法。
那么废话不多说,其实可以总结起来就是:Java语言是一门静态多分派,动态单分派的语言。
二、VM中动态分派的实现
静态分派相对好理解,只要确定静态类型、参数以及参数个数就能知道最后是哪个版本被执行,但是动态分派就相对难理解,但是结合VM的相关知识理解,其实也就那么回事。
(1)高能权威解释:
动态分配并不会频繁操作去搜索合适的目标方法,而是通过在类的方法区中建立一个虚方法表(Virtual Method Table=vtable)来代替元数据查找以提高性能。Father与Son的虚方法表如下图(Vtable中存放各个方法的实际入口地址)
(2)个人粗俗解释:
动态分派(我)懒得每次都要浪费时间去找要被执行的目标方法,VM大哥本来就已经提供给了我方法表这么一个好东西,只要我加以利用(虚表中存放方法的实际入口)就能实现了找到目标方法。
先看下图,之后解释便会一目了然:
解释1:如果子类没有重写方法,执行的是父类的方法:这是因为子类中没有被重写,那么子类的虚方法表里面的地址入口与父类相同方法的地址入口是一致的,都指向父类的实现入口。即:Father与Son的choiceA与choiceB方法都指向了Father相同方法中的入口地址。
解释2:如果子类重写父类方法,那么执行的是子类的方法,这是因为子类方法表中地址将会被替换为指向子类实现版本的入口地址。即:Son的choiceA与choiceB方法被重写了,则指向了Son实现版本的入口地址,并没有指向Father的箭头。
解释3:Father与Son从Object继承来的方法都没有重写,故都会指向Object的数据类型;