从字节码层面来看,Java中的所有方法调用,最终无外乎转换为如下几条调用指令。

  • invokestatic: 调用静态方法。
  • invokespecial: 调用实例构造器<init>方法,私有方法和父类方法。
  • invokevirtual: 调用所有的虚方法。
  • invokeinterface: 调用接口方法,会在运行时再确定一个实现此接口的对象。
  • invokedynamic: 调用动态方法。JDK 7引入的,主要是为了支持动态语言的方法调用。

JVM提供了上述5条方法调用指令,所以不妨从字节码层面来一窥Java多态机制的执行过程。

1 虚方法和非虚方法

上述5条方法调用指令中的invokevirtual负责调用所有的虚方法。那么什么是虚方法?什么是非虚方法呢?

从Java语言层面来看,static,private,final修饰的方法,父类方法以及实例构造器,这些方法称为非虚方法。与之相反,其他所有的方法称为虚方法。

字节码指令层面来讲,invokestatic和invokespecial调用的方法都是非虚方法。

2 静态类型和实际类型

先看一看以下代码的定义:

Human man = new Man();

我们把 Human称为变量的静态类型,Man称为变量的实际类型。

引用变量都有两个类型:静态类型(定义该变量的类型)和实际类型(实际指向的对象的类型)。

静态类型是编译时可知的,而实际类型只有运行时才能确定。

3 Java中多态机制的实现过程

invokevirtual指令的运行时解析过程大致分为如下几个步骤:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
  2. 如果在类型C中找到常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
  4. 如果始终没找到合适的方法,则抛出java.lang.AbstractMethodError异常。

正是由于invokevirtual指令是这样的一个执行过程,所以这就解释了为什么java语言里面实现多态需要如下三个条件:a. 父类引用指向子类对象。b. 有继承的存在。c. 子类重写父类方法。

  • 由于父类引用指向子类对象,所以jvm会去首先去查找该子类对象对应的类型。
  • 又由于有继承的存在,所以子类的方法不可能比父类少,这就保证了,只要该引用变量能调用的方法,子类中一定存在。所以第二步一定能在子类的类型中查找到调用的方法。
  • 方法找到后就可以执行了,至于方法执行后能不能产生不同的效果(多态),得看子类是否重写了这个方法。所以要想产生多态,子类得重写父类方法。【注意:以上所说的方法均是指的虚方法。】

4 静态分派和动态分派

分派过程会揭示多态的一些最基本的体现,比如”重写“和“重载”在Java虚拟机中是如何实现的。

4.1 静态分派

所有依赖(实参的)静态类型来定位方法执行版本的分派动作称为静态分派。典型的应用就是方法重载(Overload)。

先看一个例子:

public class StaticDispatch {
static 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, man!");
} public void sayHello(Women guy) {
System.out.println("hello, women!");
} public static void main(String[] args) {
Human man = new Man();
Human women = new Women();
StaticDispatch sd = new StaticDispatch();
sd.sayHello(man);
sd.sayHello(women);
} }

输出结果:

hello, guy!
hello, guy!

没错,程序就是大家熟悉的重载(Overload)。在上述程序中,由于方法的接受者已经确定是StaticDispatch的实例sd了,所以最终调用的是哪个重载版本也就取决于传入参数的类型了。

Java中重载的本质

编译器在重载时是根据传入实参的静态类型而不是实际类型作为判定依据的。静态类型是编译时可知的,所以在编译阶段,编译器会根据实参的静态类型决定调用那个重载版本。

4.2 动态分派

在运行期根据变量的实际类型来确定方法执行版本的分派过错称为动态分派。典型的应用就是重写(override)。例子如下:

public class DynamicDispatch {

    public static void main(String[] args) {

        Human man = new Man();
Human women = new Women(); man.sayHello();
women.sayHello(); man = new Women();
man.sayHello(); } } abstract class Human {
protected abstract void sayHello();
} class Man extends Human { @Override
protected void sayHello() {
System.out.println("hello man!");
} } class Women extends Human { @Override
protected void sayHello() {
System.out.println("hello women!");
} }

输出结果:

hello man!
hello women!
hello women!

Java中重写的本质

见invokevirtual指令的运行时的解析过程。

4.3 综合例子

public class Dispatch {

    static class QQ{}
static class _360{}
public static class Father{ public void hardChoice(_360 _360) {
System.out.println("Father choose 360");
} public void hardChoice(QQ qq) {
System.out.println("Father choose qq");
} } public static class Son extends Father{ public void hardChoice(_360 _360) {
System.out.println("Son choose 360");
} public void hardChoice(QQ qq) {
System.out.println("Son choose qq");
} } public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
} }

输出结果:

Father choose 360
Son choose qq

分析如下:

     Father father = new Father();
Father son = new Son(); /**
* Father里面有两个重载的hardChoice方法。所以会根据hardChoice()的实参的【静态类型】来决定调用哪个版本的方法。
*/
father.hardChoice(new _360()); /**
* 变量son的静态类型是Father,实际类型是Son。并且类Son重写了父类Father里面的两个重载的hardChoice方法。
* 所以运行的时候首先会确定调用子类Son里面的方法,然后在根据hardChoice()的实参的【静态类型】来决定调用Son里面的哪个版本的方法。
*/
son.hardChoice(new QQ());

5 虚拟机动态分派的实现

  • 由于动态分派是非常频繁的操作,基于性能考虑,在JVM的具体实现中常常 做一些优化。最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称vtable)。与此对应的,invokeinterface执行时也会用到接口方法表(Interface Method Table,简称itable)。
  • 虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么子类的虚方法表里的地址入口和父类相同方法的地址入口是一致的。如果子类重写了这个方法,子类方法表中的地址就会被替换为指向子类实现版本的入口地址。

参考资料

http://blog.csdn.net/kobejayandy/article/details/39620679

http://www.cnblogs.com/jack204/archive/2012/10/29/2745150.html

http://blog.csdn.net/oypc2303/article/details/4393831

http://blog.csdn.net/lidaweihgy/article/details/7660346

http://www.cnblogs.com/qinqinmeiren/archive/2011/07/15/2151687.html

http://www.geekcome.com/content-10-4128-1.html

深入理解Java多态机制的更多相关文章

  1. 深入理解java多态没有烤山药的存在,java就不香了吗?

    目录 1. 从吃烤山药重新认识多态 2. 多态前提条件[重点] 3. 多态的体现 4. 多态动态绑定与静态绑定 5. 多态特性的虚方法(virtual) 7. 向上转型 8. 向下转型 9. 向上向下 ...

  2. 从零开始理解JAVA事件处理机制(2)

    第一节中的示例过于简单<从零开始理解JAVA事件处理机制(1)>,简单到让大家觉得这样的代码简直毫无用处.但是没办法,我们要继续写这毫无用处的代码,然后引出下一阶段真正有益的代码. 一:事 ...

  3. 从零开始理解JAVA事件处理机制(3)

    我们连续写了两小节的教师-学生的例子,必然觉得无聊死了,这样的例子我们就是玩上100遍,还是不知道该怎么写真实的代码.那从本节开始,我们开始往真实代码上面去靠拢. 事件最容易理解的例子是鼠标事件:我们 ...

  4. 理解Java类加载机制(译文)

    理解java类加载机制 你想写类加载器?或者你遇到了ClassCastException异常,或者你遇到了奇怪的LinkageError状态约束异常.应该仔细看看java类的加载处理了. 什么是类加载 ...

  5. Java基础 -- 深入理解Java异常机制

    异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通 过API中Throwable类的众多子类描述各种不同的异常. ...

  6. 【转】深入理解java异常处理机制

    深入理解java异常处理机制 ; int c; for (int i = 2; i >= -2; i--) { c = b / i; System.out.println("i=&qu ...

  7. 转:一个经典例子让你彻彻底底理解java回调机制

    一个经典例子让你彻彻底底理解java回调机制 转帖请注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming/article/details/17483273 ...

  8. 深入理解Java流机制(一)

    一.前言 C语言本身没有输入输出语句,而是调用"stdio.h"库中的输入输出函数来实现.同样,C++语言本身也没有输入输出,不过有别于C语言,C++有一个面向对象的I/O流类库& ...

  9. 理解Java反射机制

    理解Java反射机制 转载请注明出处,谢谢! 一.Java反射简介 什么是反射? Java的反射机制是Java特性之一,反射机制是构建框架技术的基础所在.灵活掌握Java反射机制,对学习框架技术有很大 ...

随机推荐

  1. django自带wsgi server vs 部署uwsgi+nginx后的性能对比

    一.下面先交代一下测试云主机 cpu: root@alexknight:/tmp/webbench-1.5# cat /proc/cpuinfo |grep model model : model n ...

  2. bootstrap div 弹出与关闭

    html <div id="myModal" class="modal" tabindex="-1" role="dialo ...

  3. 快速判断素数 --Rabin-Miller算法

    以前我在判断素数上一直只会 sqrt(n) 复杂度的方法和所谓的试除法(预处理出sqrt(n)以内的素数,再用它们来除). (当然筛选法对于判断一个数是否是素数复杂度太高) 现在我发现其实还有一种方法 ...

  4. 【MySql】C#数据库备份与还原

    public static class SQLBackup { /// <summary> /// 执行Cmd命令 /// </summary> /// <param n ...

  5. 安装Docker Toolbox后出现的问题

    Installing Docker Toolbox on Windows with Hyper-V Installed Installing Docker on Windows is a fairly ...

  6. [py]给函数传递数组和字典

    一 , 1.1传元组 def fun(x): print x t=(1,2) fun(t) 1.2传元组 #传元组 def fun(x,y): print x,y # t=(1,2) t=(1,2,3 ...

  7. JS导出Excel 代码笔记

    var tableToExcel = (function () { var uri = 'data:application/vnd.ms-excel;base64,', template = '< ...

  8. RHEL每天定时备份Oracle

    步骤: (1)创建脚本文件bak_112.sh,内容如下(自动按当前日期备份数据库): #!/bin/sh export ORACLE_BASE=/u01/app/oracle; ORACLE_HOM ...

  9. GWT-Dev-Plugin(即google web toolkit developer plugin)for firefox的下载地址

    如果FireFox的版本为20,则对应google-web-toolkit的插件离线下载地址,不要用浏览器直接下载,用Flashget等客户端下载,超快. http://google-web-tool ...

  10. android服务之启动方式

    服务有两种启动方式 通过startService方法来启动 通过bindService来开启服务 布局文件 在布局文件中我们定义了四个按键来测试这两种方式来开启服务的不同 <?xml versi ...