从字节码层面来看,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. c语言:printf系列的函数

    /** *----------------------------stdio.h--------------------------------------- * int printf(const c ...

  2. Debian8.2 下的软件配置

    Add "ll" to alias: ~/.bashrc里面实际上已经有这个alias,把注释去掉就可以了 小红点(指点杆)的启用 这个版本可以在系统配置里把触摸板关掉, 但是这个 ...

  3. 微软职位内部推荐-SDEII_ ECO

    微软近期Open的职位: SDE II SDE II Organization Summary: Engineering, Customer interactions & Online (EC ...

  4. Dell 服务器做Raid

    Dell 服务器做Raid DELL R720 服务器 RAID阵列卡配置介绍 (H310) 关于 RAID 5 与热备份(Hot Spare) 在不同RAID组间使用热备盘——Global Hot ...

  5. Android 的图片异步请求加三级缓存 ACE

    使用xUtils等框架是很方便,但今天要用代码实现bitmapUtils 的功能,很简单, 1 AsyncTask请求一张图片 ####AsyncTask #####AsyncTask是线程池+han ...

  6. Java程序设计的DOS命令基础

    Java程序设计的DOS命令基础 用户使用操作系统和软件有两种方式:命令行界面(Command Line Interface,CLI)和图形界面(Graphical User Interface,GU ...

  7. ios蓝牙开发(四)app作为外设被连接的实现-转发

    代码下载: 原博客中大部分示例代码都上传到了github,地址是:https://github.com/coolnameismy/demo. 再上一节说了app作为central连接periphera ...

  8. SQLServer(MSSQL)、MySQL、SQLite、Access相互迁移转换工具 DB2DB v1.2

    最近公司有一个项目,需要把原来的系统从 MSSQL 升迁到阿里云RDS(MySQL)上面.为便于测试,所以需要把原来系统的所有数据表以及测试数据转换到 MySQL 上面.在百度上找了很多方法,有通过微 ...

  9. 3Dmax 创建物体

    扩展基本体-切角长方体: 增加边: 删除边:在边选择模式下, 选择想要删除的边, 按下ctrl+backsapce

  10. 从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值

    具体的错误原因是:C#中的DateTime类型比SqlServer中的datetime范围大.SqlServer的datetime有效范围是1753年1月1日到9999年12月31日,如果超出这个范围 ...