从字节码层面来看,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. Mysql备份系列(3)--innobackupex备份mysql大数据(全量+增量)操作记录

    在日常的linux运维工作中,大数据量备份与还原,始终是个难点.关于mysql的备份和恢复,比较传统的是用mysqldump工具,今天这里推荐另一个备份工具innobackupex.innobacku ...

  2. DEDECMS之二 如何修改模板页

    使用织梦系统最经常是为了仿站,那么模板应该怎么改? 这里主要谈谈关于比较常用的几个模板页 网站主页.列表页.内容页.栏目的调用 1.主页模板 常用组合方法:index.htm + head.htm + ...

  3. ubuntu15.10 或者 16.04 或者 ElementryOS 下使用 Dotnet Core

    这里我们不讲安装,缺少libicu52自行安装. 安装完成后使用dotnet restore或者build都会失败,一是报编译的dll不适合当前系统,二是编译到ubuntu16.04文件夹下会产生一些 ...

  4. 更便捷的Android多渠道打包方式

    本文先回顾了以往流行的多渠道打包方式,随后引入的mcxiaoke的packer-ng-plugin项目,介绍该项目在实际应用(配合友盟统计)中如何解决更方便的Android多渠道打包问题 多渠道打包方 ...

  5. 「拉勾网」薪资调查的小爬虫,并将抓取结果保存到excel中

    学习Python也有一段时间了,各种理论知识大体上也算略知一二了,今天就进入实战演练:通过Python来编写一个拉勾网薪资调查的小爬虫. 第一步:分析网站的请求过程 我们在查看拉勾网上的招聘信息的时候 ...

  6. 青瓷引擎之纯JavaScript打造HTML5游戏第二弹——《跳跃的方块》Part 10(排行榜界面&界面管理)

    继上一次介绍了<神奇的六边形>的完整游戏开发流程后(可点击这里查看),这次将为大家介绍另外一款魔性游戏<跳跃的方块>的完整开发流程. (点击图片可进入游戏体验) 因内容太多,为 ...

  7. 高仿Windows Phone QQ登录界面

    给 TextBox文本框前添加图片 扩展PhoneTextBox:添加一个类"ExtentPhoneTextBox"继承 PhoneTextBox ,在"ExtentPh ...

  8. HDU5892~HDU5901 2016网络赛沈阳

    A.题意: 有一个n×n的格子, 有50种怪物. 有m个操作, 每次操作会往一个矩形区域放怪物, 每个格子放相同数目的怪物, 或者查询当前50种怪物的奇偶性. 分析:用2^50表示怪物的奇偶,然后就是 ...

  9. java代码注释规范

    java代码注释规范   代码注释是架起程序设计者与程序阅读者之间的通信桥梁,最大限度的提高团队开发合作效率.也是程序代码可维护性的重要环节之一.所以我们不是为写注释而写注释.下面说一下我们在诉求网二 ...

  10. 浅谈Javascript 中几种克隆(clone)方式

    clone就是把原来的东西原样复制一份,新复制的东西和以前的东西没有任何关系 一:在Javascript里,如果克隆对象是基本类型,我们直接赋值就可以了: var sStr = "kingw ...