Java多态实现原理

待兔 等级 615 0 0
标签: 多态接口Java

Java多态概述

多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。Java 对于方法调用动态绑定的实现主要依赖于方法表,但通过类引用调用(invokevirtual)和接口引用调用(invokeinterface)的实现则有所不同。

类引用调用的大致过程为:Java编译器将Java源代码编译成class文件,在编译过程中,会根据静态类型将调用的符号引用写到class文件中。在执行时,JVM根据class文件找到调用方法的符号引用,然后在静态类型的方法表中找到偏移量,然后根据this指针确定对象的实际类型,使用实际类型的方法表,偏移量跟静态类型中方法表的偏移量一样,如果在实际类型的方法表中找到该方法,则直接调用,否则,认为没有重写父类该方法。按照继承关系从下往上搜索。

接口引用调用后面再说吧。

从上图可以看出,当程序运行时,需要某个类时,类载入子系统会将相应的class文件载入到JVM中,并在内部建立该类的类型信息(这个类型信息其实就是class文件在JVM中存储的一种数据结构),包含java类定义的所有信息,包括方法代码,类变量、成员变量、以及本博文要重点讨论的方法表。这个类型信息就存储在方法区。

注意,这个方法区中的类型信息跟在堆中存放的class对象是不同的。在方法区中,这个class的类型信息只有唯一的实例(所以是各个线程共享的内存区域),而在堆中可以有多个该class对象。可以通过堆中的class对象访问到方法区中类型信息。就像在java反射机制那样,通过class对象可以访问到该类的所有信息一样。
【重点】
方法表是实现动态调用的核心。上面讲过方法表存放在方法区中的类型信息中。为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向一个记录该类方法的方法表,方法表中的每一个项都是对应方法的指针。
这些方法中包括从父类继承的所有方法以及自身重写(override)的方法。
【拓展】
方法区:方法区和JAVA堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
运行时常量池:它是方法区的一部分,Class文件中除了有类的版本、方法、字段等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分信息在类加载时进入方法区的运行时常量池中。
方法区的内存回收目标是针对常量池的回收及对类型的卸载。

Java 的方法调用方式

Java 的方法调用有两类,动态方法调用与静态方法调用。

  • 静态方法调用是指对于类的静态方法的调用方式,是静态绑定的
  • 动态方法调用需要有方法调用所作用的对象,是动态绑定的。
    类调用 (invokestatic) 是在编译时就已经确定好具体调用方法的情况。
    实例调用 (invokevirtual)则是在调用的时候才确定具体的调用方法,这就是动态绑定,也是多态要解决的核心问题。
    JVM 的方法调用指令有四个,分别是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前两个是静态绑定,后两个是动态绑定的。本文也可以说是对于JVM后两种调用实现的考察。
方法表与方法调用

如有类定义 Person, Girl, Boy

class Person {
    public String toString() {
        return "I'm a person.";
    }

    public void eat() {
    }

    public void speak() {
    }

}

class Boy extends Person {
    public String toString() {
        return "I'm a boy";
    }

    public void speak() {
    }

    public void fight() {
    }
}

class Girl extends Person {
    public String toString() {
        return "I'm a girl";
    }

    public void speak() {
    }

    public void sing() {
    }
} 

当这三个类被载入到 Java 虚拟机之后,方法区中就包含了各自的类的信息。Girl 和 Boy 在方法区中的方法表可表示如下:

可以看到,Girl 和 Boy 的方法表包含继承自 Object 的方法,继承自直接父类 Person 的方法及各自新定义的方法。注意方法表条目指向的具体的方法地址,如 Girl 继承自 Object 的方法中,只有 toString() 指向自己的实现(Girl 的方法代码),其余皆指向 Object 的方法代码;其继承自于 Person 的方法 eat() 和 speak() 分别指向 Person 的方法实现和本身的实现。

如果子类改写了父类的方法,那么子类和父类的那些同名的方法共享一个方法表项。

因此,方法表的偏移量总是固定的。所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。
Person 或 Object中的任意一个方法,在它们的方法表和其子类 Girl 和 Boy 的方法表中的位置 (index) 是一样的。这样 JVM 在调用实例方法其实只需要指定调用方法表中的第几个方法即可。

如调用如下:

class Party {
    void happyHour() {
        Person girl = new Girl();
        girl.speak();
    }
} 

当编译 Party 类的时候,生成 girl.speak()的方法调用假设为:

Invokevirtual #12

设该调用代码对应着 girl.speak(); #12 是 Party 类的常量池的索引。JVM 执行该调用指令的过程如下所示:

(1)在常量池(这里有个错误,上图为ClassReference常量池而非Party的常量池)中找到方法调用的符号引用 。 (2)查看Person的方法表,得到speak方法在该方法表的偏移量(假设为15),这样就得到该方法的直接引用。
(3)根据this指针得到具体的对象(即 girl 所指向的位于堆中的对象)。
(4)根据对象得到该对象对应的方法表,根据偏移量15查看有无重写(override)该方法,如果重写,则可以直接调用(Girl的方法表的speak项指向自身的方法而非父类);如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,同样按照这个偏移量15查看有无该方法。

接口调用

因为 Java 类是可以同时实现多个接口的,而当用接口引用调用某个方法的时候,情况就有所不同了。

Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同样的方法在基类和派生类的方法表的位置就可能不一样了

interface IDance {
    void dance();
}

class Person {
    public String toString() {
        return "I'm a person.";
    }

    public void eat() {
    }

    public void speak() {
    }

}

class Dancer extends Person implements IDance {
    public String toString() {
        return "I'm a dancer.";
    }

    public void dance() {
    }
}

class Snake implements IDance {
    public String toString() {
        return "A snake.";
    }

    public void dance() {
        //snake dance  
    }
} 

可以看到,由于接口的介入,继承自于接口 IDance 的方法 dance()在类 Dancer 和 Snake 的方法表中的位置已经不一样了,显然我们无法仅根据偏移量来进行方法的调用。

Java 对于接口方法的调用是采用搜索方法表的方式,如,要在Dancer的方法表中找到dance()方法,必须搜索Dancer的整个方法表。

因为每次接口调用都要搜索方法表,所以从效率上来说,接口方法的调用总是慢于类方法的调用的。

收藏
评论区

相关推荐

Caused by: java.lang.RuntimeException: xxl-job jobhandler naming conflicts.
xxljob报这个错误,是因为jobHandler名字重复了,记录一下。
java.lang.IllegalStateException: Ambiguous mapping found. Cannot map 'XXXXX' bean
mvc项目启动报这个错原因:rest/controller中映射的地址重复
HelloWorld
Finally find this place.Nice to meet you all.关注helloworld社区也有一段时间了,由于是新晋社区资源较少,一直没有怎么真正的去使用,只是单纯觉得界面美观简洁,符合自己的审美,偶尔逛逛而已。今天终于有时间可以可以坐下来说说话,都说程序员的第一行输出是helloworld,在这个社区的第一篇博客我也
垃圾回收机制
GC标记算法 对象被判定为垃圾的标准:没有被其他对象引用 引用计数算法: 判断对象的引用数量: 通过判断对象的引用数量来决定对象是否可以被回收 每个对象实例都有一个引用计数器,被引用则1,完成引用则1 任何引用计数为0的对象实例可以被当做垃圾收集 优点:执行效率高,程序执行受影响较小。
如何设计一个数据库?
设计两个大模块,存储(文件系统)与程序的实例模块。程序的实例模块划分为:存储管理,缓存机制,SQL解析,日志管理,权限划分,容灾机制,索引管理,锁管理。 为什么使用索引? 假设使用原始的全表查询,那么对于小量数据可能速度并没有影响,但是在大量数据的情况下会使得速度很慢。而索引,则类似于字典中的偏旁部首,加快了查询的效率。 二叉
第一天:今天学spring的时候遇到一个错误
具体错误如下:Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'testDao' is defined at org.springframework.beans.factory.support.D
Java 基础 SDK区别简介
1. Java SE(Java Platform,Standard Edition),应该先说这个,因为这个是标准版本。 Java EE (Java Platform,Enterprise Edition),java 的企业版本 Java ME(Java Platform,Micro Edition),java的微型版本。 1). JavaSE 可以
Java 高级应用编程 第一章 工具类
**一、Java API** **Java API简介** 1、API (Application Programming Interface) 应用程序接口 2、Java中的API,就是JDK提供的各种功能的Java类 3、JDK帮助文档   JAVA\_API\_CN.chm   官网地址http://www.oracle.com/tech
java 配置環境
JAVA\_HOME                                      C:\\Java\\jdk1.8.0\_11 PATH:                                                 %JAVA\_HOME%\\bin;%JAVA\_HOME%\\jre\\bin; CLASSPATH: 
java基础知识随身记
2018年11月12日20:51:35 一、基础知识: 1、JVM、JRE和JDK的区别: JVM(Java Virtual Machine):java虚拟机,用于保证java的跨平台的特性。   java语言是跨平台,jvm不是跨平台的。 JRE(Java Runtime Environment):java的运行环境,包括jvm+java的核心类
java学习第一步
Java SE 磨刀不误砍柴工,工欲善其事必先利其器,咱们先搞好硬件配置,才能顺利的搞好Java学习 阶段一 1、认识Java 2、java发展史及用户 3、配置Java环境 4、JDK8下载安装 5、配置环境变量     JAVA\_HOME配置         java\_home配置两种方法:                 1、J
java的特性
java的特性 ======= 1、Java语言是简单的 2、Java语言是面向对象的 3、Java语言是分布式的 4、Java语言是健壮的 5、Java语言是安全的 6、Java语言是可移植性的 7、Java语言是解释型的 8、Java语言是多线程的 9、Java语言是动态的语言   **Java语言是简单的:**  
How to switch java version on ubuntu 20.04
* View all java versions lwk@qwfys:~$ update-java-alternatives --list java-1.11.0-openjdk-amd64 1111 /usr/lib/jvm/java-1.11.0-openjdk-amd64 java-1.8.0-ope
Jmeter使用过程中遇到的问题
**问题一:** 已安装java,也配置了java环境变量,但打开jmeter时提示:Not able to find Java executable or version. **解决方法:** 在jmeter.bat文件前面加上以上两句: SET JAVA\_HOME=C:\\Program Files\\Java\\jdk1.7.0\_51 S
Linux中查看jdk安装目录、Linux卸载jdk、rpm命令、rm命令参数
一、查看jdk安装目录 ----------- [root@node001 ~]# whereis java java: /usr/bin/java /usr/local/java #java执行路径 [root@node001 ~]# which java /usr/bin/java #查看JDK安装路径