我们都知道Java有三大宝,分别是:多态、封装、继承。其中多态主要体现就是重写与重载(有些人认为重载根本不属于多态)两种方式,那么今天就结合研读过JVM之后自己的理解来聊聊重载与重写的VM内部是怎么实现的,是怎么找到最后执行的方法的。

  在分析重载与重写这两个之前,我们必须要知道一些概念:分派、静态分派、动态分派、实际类型、静态类型....(之后涉及到的会边介绍别举例才能更好地理解)

一、相关的概念

1、静态类型与实际类型

  先看以下一个重载例子的输出,再进一步介绍:

 public class StaticDispatch {

     static abstract class Human{  }
static class Man extends Human{ }
static class Women extends Human{ }
//三个方法重载
public void sayHello(Human guy) {
System.out.println("hello guy!");
}
public void sayHello(Man guy) {
System.out.println("hello gentleman!");
}
public void sayHello(Women guy) {
System.out.println("hello laday!");
}
public static void main(String[] args) {
Human man = new Man();//upcast
Human woman = new Women();//upcast
StaticDispatch sd = new StaticDispatch();
//输出结果为:hello guy!
sd.sayHello(man);
sd.sayHello(woman);
}
}

大多数都应该能知道最后的输出结果为:

hello, guy
hello, guy

这是为什么呢?根据输出来看man与woman就是Human类,这肯定是成立的,通过向上转型成基类(父类)可以使用getClass()或者instanceof确定,我们先不研究为什么吧。再通过重写的一个例子:

 public class DynamicDispatch {
static abstract class Human{
protected void sayHello("hello, guy");
}
static class Man extends Human{ @Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Women extends Human{ @Override
protected void sayHello() {
System.out.println("women say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human women = new Women();
man.sayHello();
women.sayHello();
man = new Women();
man.sayHello();
}

输出结果:

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便能理解!

  那么如果不重写父类的方法呢?

 public class DynamicDispatch {
static abstract class Human{
protected void sayHello() {
System.out.println("hello, guy");
};
}
static class Man extends Human{ /*@Override
protected void sayHello() {
//System.out.println("man say hello");
} */
}
static class Women extends Human{ /*@Override
protected void sayHello() {
//System.out.println("women say hello");
}*/
}
public static void main(String[] args) {
Human man = new Man();
Human women = new Women();
man.sayHello();
women.sayHello();
man = new Women();
man.sayHello();
}
}

结果为:

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的数据类型;

  

深入理解Java重载与重写的更多相关文章

  1. java 重载、重写、构造函数详解

    方法重写 1.重写只能出现在继承关系之中.当一个类继承它的父类方法时,都有机会重写该父类的方法.一个特例是父类的方法被标识为final.重写的主要优点是能够定义某个子类型特有的行为. class An ...

  2. Java——重载和重写

    前言 在程序设计中经常会遇到对对方法的重载或者重写,下面将介绍重载和重写. 重载(Overloade) 重载出现的原因 任何程序设计语言都具备的一项重要特性就是对名字的运用.当创建一个对象时,就给对象 ...

  3. java 重载、重写、重构的区别

    1.重载 构造函数是一种特殊的函数,使用构造函数的目的是用来在对象实例化时初始化对象的成员变量.由于构造函数名字必须与类名一致,我们想用不同的方式实例化对象时,必须允许不同的构造方法同时存在,这就用到 ...

  4. 一句话学Java——Java重载和重写

    概念:重载是指两个不同的函数有相同的名称,可以是在本类之中的函数之间的重载,也可以是子类和父类的函数之间的函数重载. 重写:只能是子类重写父类的函数.这是多态的基础. 重写的规则:     参数:重写 ...

  5. 漫谈java重载与重写

    重载(Overloading):为了让方法名相同而形参不同的构造方法同时存在,让类以统一的方式处理不同类型数据的一种手段 重写(Overriding):导出类对继承自基类的方法做出一定的修改,又称方法 ...

  6. java重载和重写的区别

    一.重载(Overloading) (1) 方法重载是让类以统一的方式处理不同类型数据的一种手段.多个同名函数同时存在,具有不同的参数个数/类型. 重载Overloading是一个类中多态性的一种表现 ...

  7. java 重载与重写 【转】

    首先我们来讲讲:重载(Overloading) (1) 方法重载是让类以统一的方式处理不同类型数据的一种手段.多个同名函数同时存在,具有不同的参数个数/类型. 重载Overloading是一个类中多态 ...

  8. java重载和重写

    重载(Overloading) (1) 方法重载是让类以统一的方式处理不同类型数据的一种手段.多个同名函数同时存在,具有不同的参数个数/类型. 重载Overloading是一个类中多态性的一种表现. ...

  9. JAVA构造器,重载与重写

    1. java构造器 构造器也叫构造方法(constructor), 用于对象初始化. 构造器是一个创建对象时被自动创建的特殊方法,目的是对象的初始化. 构造器 的名称与类的名称一致. JAVA通过n ...

随机推荐

  1. TensorFlow数据读取

    TensorFlow高效读取数据的方法 TF Boys (TensorFlow Boys ) 养成记(二): TensorFlow 数据读取 Tensorflow从文件读取数据 极客学院-数据读取 十 ...

  2. vue使用路由跳转到上一页

    this.$router.go(-1) <template> <div> <button class="btn btn-success" @click ...

  3. ASP.NET Core 请求/查询/响应参数格式转换(下划线命名)

    业务场景: 在 ASP.NET Core 项目中,所有的代码都是骆驼命名,比如userName, UserName,但对于 WebApi 项目来说,因为业务需要,一些请求.查询和响应参数的格式需要转换 ...

  4. 在Git中设置自己的姓名

    在Git中,自己的姓名与每一个commit提交绑定在一起.如果你在使用Azure DevOps Server中的Git Repo时,一定要注意commit中的提交者与服务器上的推送者,是两个概念. 在 ...

  5. 重新编译安装swoole支持OpenSSL

    1.下载:wget http://pecl.php.net/get/swoole-1.9.22.tgz 2.解压:tar zxvf swoole-1.9.22.tgz 3.扩展模块:cd swoole ...

  6. linux(centos7)安装docker

    1.检查内核版本,必须是3.10及以上 uname ‐r 2.安装docker yum install docker 3.输入y确认安装 4.启动docker [root@localhost ~]# ...

  7. Trinity的分步运行

    当使用Trinity组装时,如果数据量过大,可以考虑使用--min_kmer_cov 2参数丢弃uniquely occurring kmer, 从而降低内存消耗 设置--no_distributed ...

  8. iOS调用QQ发起临时会话

    iOS调用QQ发起临时会话 iOS调用qq前先判断是否安装qq, 之后通过OpenURL打开对用的qq NSURL *url = [NSURL URLWithString:@"mqq://& ...

  9. DOMContentLoaded事件中使用异步

    概述 我在之前的博文(Performance面板看js加载)中提到过,如果利用监听DOMContentLoaded事件的方式来加载js是不能优化加载的,不能够替代jquery中的ready方法,原因是 ...

  10. [CocoaPods]故障排除

    安装CocoaPods 如果您在macOS 10.9.0-10.9.2上安装,当RubyGems尝试安装jsongem 时可能会遇到问题.要解决此问题,请遵循以下说明 从macOS 10.8升级到10 ...