前言对于习惯使用面向对象开发的工程师们来说,重载 & 重写 这两个概念应该不会陌生了。在中 / 低级别面试中,也常常会考察面试者对它们的理解(隐约记得当年在校招面试时遇到过);

网上大多数资料 & 面经对这两个概念的阐述,多数仅停留在讨论两者在 表现上 的差异,让读者去被动地接受知识。在这篇文章里,我将更有深度地理解重载 & 重写的原理,应深入理解Java 虚拟机执行引擎是如何进行方法调用的。请点赞,你的点赞和关注真的对我非常重要!

首先,尝试写出以下程序的输出:

public class Base {

public static void funcStatic(String str){

System.out.println("Base - funcStatic - String");

}

public static void funcStatic(Object obj){

System.out.println("Base - funcStatic - Object");

}

public void func(String str){

System.out.println("Base - func - String");

}

public void func(Object obj){

System.out.println("Base - func - Object");

}

}

public class Child extends Base {

public static void funcStatic(String str){

System.out.println("Child - funcStatic - String");

}

public static void funcStatic(Object obj){

System.out.println("Child - funcStatic - Object");

}

@Override

public void func(String str){

System.out.println("Child - func - String");

}

@Override

public void func(Object obj){

System.out.println("Child - func - Object");

}

}

public class Test{

public static void main(String[] args){

Object obj = new Object();

Object str = new String();

Base base = new Base();

Base child1 = new Child();

Child child2 = new Child();

base.funcStatic(obj); // 正常编程中不应该用实例去调用静态方法

child1.funcStatic(obj);

child2.funcStatic(obj);

base.func(str);

child1.func(str);

child2.func(str);

}

}

程序输出:

Base - funcStatic - Object

Base - funcStatic - Object

Child - funcStatic - Object

Base - func - Object

Child - func - Object

Child - func - Object

程序输出是否与你的预期一致呢?遇到困难了吗,相信这篇文章一定能帮到你...

延伸文章

目录

1. 静态类型 & 实际类型

每一个变量都有两种类型:静态类型(Static Type) & 实际类型(Actual Type)。例如下面代码中,Base为变量base的静态类型,Child为实际类型:

Base base = new Child();

两者的具体区别如下: - 静态类型:引用变量的类型,在编译期确定,无法改变 - 实际类型:实例对象的类型,在编译期无法确定,需在运行期确定,可以改变

这里先谈到这里,后文会从字节码的角度理解继续讨论两个类型。

2. 方法调用的本质

这一节,我们来讨论Java中方法调用的本质。我们知道,Java前端编译的产物是字节码,与C/C++不同,前端编译过程中并没有链接步骤,字节码中所有的方法调用都是使用符号引用。举个例子:

- 源码:

public class Child extends Base {

@Override

void func() {

}

void test1(){

func();

}

void test2(){

super.func();

}

}

- 字节码(javap -c Child.class):

Compiled from "Child.java"

public class com.Child extends com.Base {

// 构造函数,默认调用父类构造函数

public com.Child();

Code:

0: aload_0

1: invokespecial #1 // Method com/Base."":()V

4: return

void func();

Code:

0: return

void test1();

Code:

0: aload_0

// invokevirtual 调用实例方法

1: invokevirtual #2 // Method func:()V

4: return

void test2();

Code:

0: aload_0

// invokespecial 调用静态方法

1: invokespecial #3 // Method com/Base.func:()V

4: return

}

上面的字节码中,invokespecial和invokevirtual都是方法调用的字节码指令,具体细节下文会详细解释。后面的#1 #2 #3表示符号引用在常量池中的索引号,根据这个索引号检索常量表,可以查到最终表示的是一个字符串字面量,例如func:()V,这个就是方法的符号引用。为了方便理解字节码,javap反编译的字节码已经在注释中提示了最终表示的值,例如Method func:()V。

符号引用(Symbolic References)是一个用来无歧义地标识一个实体(例如方法/字段)的字符串,在运行期它会翻译为直接引用(Direct Reference)。对于方法来说,就是方法的入口地址。

下图描述了方法符号引用的基本格式:方法符号引用

这个符号引用包含了变量的静态类型(如果是变量的静态类型与本类相同,不需要指明)、简单方法名以及描述符(参数顺序、参数类型和方法返回值)。通过这个符号引用,Java虚拟机就可以翻译出该方法的直接引用。但是,同一个符号引用,运行时翻译出来的直接引用可能是不同的,为什么会这样呢?小结: 1. 方法调用的本质是根据方法的符号引用确定方法的直接引用(入口地址)

3. 从符号引用到直接引用

为什么同一个符号引用,运行时翻译出来的直接引用可能是不同的?这与使用的方法调用指令的处理过程有关,Java字节码的方法调用指令一共有以下 5 种:

其中,根据调用方法的版本是否在编译期可以确定,(注意:只是版本,而不是入口地址,入口地址只能在运行时确定)可以将方法调用划分为静态解析 & 动态分派两种。# 误区(重要)#

《深入理解Java虚拟机》中将方法调用分为解析、静态分派、动态分派三种,又根据宗量的数量引入了静态多分派,动态单分派的概念。这些概念事实上过于字典化,也很容易让读者误认为静态分派与动态分派是非此即彼的互斥关系。事实上,一个方法可以同时重写与重载 ,重载 & 重写是方法调用的两个阶段,而不是两个种类。

下面,我将介绍Java中方法选择的三个步骤:

3.1 步骤1:生成符号引用(编译时)

上一节我们提到过方法符号引用的基本格式,分为三个部分: - 变量的静态类型: 类的全限定名中将.替换为/,例如java.lang.Object对应java/lang/Object - 简单名称: 方法的名称,例如Object#toString()的简单名称为:toString - 描述符: 方法的参数列表和返回值,例如Object#toString()的描述符为()LJava/lang/String;

描述符的规则不是本文重点,这里便不再赘述了,若不了解可阅读延伸文章。这里我们用两段程序验证上述规则,这两段程序中我们考虑了重载 & 重写、静态 & 实例两个维度的因素:

程序一(重载 & 重写)

public class Base {

public void func() {}

public void func(int i){}

}

public class Child extends Base {

@Override

public void func() {}

@Override

public void func(int i){}

}

public class Test{

public static void main(String[] args){

Base base1 = new Base();

Base child1 = new Child();

Child child2 = new Child();

base1.func(); // invokevirtual com.Base.func:():V

child1.func(); // invokevirtual com.Base.func:():V

child2.func(); // invokevirtual com.Child.func:():V

base1.func(1); // invokevirtual com.Base.func:(I):V

child1.func(1); // invokevirtual com.Base.func:(I):V

child2.func(1); // invokevirtual com.Child.func:(I):V

}

}

可以看到,符号引用中的类名确实是变量的静态类型,而不是变量的实际类型;方法名不用多说,方法描述符则选择重载方法中最合适的一个方法。这个例程很容易判断重载方法选择结果,具体选择规则其实更为复杂。

程序二(静态 & 实例)

public class Base {

public static void func() {}

public void func(int i){}

}

public class Child extends Base {

public static void func() {}

@Override

public void func(int i){}

}

public class Test{

public static void main(String[] args){

Base base1 = new Base();

Base child1 = new Child();

Child child2 = new Child();

符号引用与程序一相同,仅指令不同

base1.func(); // invokestatic com.Base.func:():V

child1.func(); // invokestatic com.Base.func:():V

child2.func(); // invokestatic com.Child.func:():V

base1.func(1); // invokevirtual com.Base.func:(I):V

child1.func(1); // invokevirtual com.Base.func:(I):V

child2.func(1); // invokevirtual com.Child.func:(I):V

}

}

可以看到,static对符号引用没有影响,仅影响使用的指令(静态方法调用使用invokestatic)。而通过对象实例去调用静态方法是javac的语法糖,编译时会转换为使用变量的静态类型固化到符号引用中。小结: 1. 方法的符号引用在编译期确定,并固化到字节码中方法调用指令的参数中 2. 是否有static修饰对符号引用没有影响,仅影响使用的字节码指令,对象实例去调用静态方法是javac的语法糖

3.2 步骤二:解析(类加载时)

为什么静态方法、私有实例方法、实例构造器、父类方法以及final修饰这五种方法(对应的关键字: static、private、、super、final)可以在编译期确定版本呢?因为无论运行时加载多少个类,这些方法都保证唯一的版本:

既然可以确定方法的版本,虚拟机在处理invokestatic、invokespecial、invokevirtual(final)时,就可以提前将符号引用转换为直接引用,不必延迟到方法调用时确定,具体来说,是在类加载的解析阶段完成转换的。

invokestatic 指令1)类加载解析阶段:根据符号引用中类名(如下例中java/lang/String变量的静态类型中),在对应的类中找到简单名称与描述符相符合的方法,如果找到则将符号引用转换为直接引用;否则,按照继承关系从下往上依次在各个父类中搜索

2)调用阶段:符号引用已经转换为直接引用;调用invokestatic不需要将对象加载到操作数栈,只需要将所需要的参数入栈就可以执行invokestatic指令。例如:

源码:

String str = String.valueOf("1")

字节码:

0: iconst_1

1: invokestatic #2 // Method java/lang/String.valueOf:(I)Ljava/lang/String;

4: astore_1

invokespecial 指令1)类加载解析阶段:同invokestatic,也是从符号引用中的静态类型开始查找

2)调用阶段:同invokestatic,符号引用已经转换为直接引用;、父类方法、私有实例方法这3种情况都是属于实例方法,所以调用invokespecial指令需要将对象加载到操作数栈。例如:

1、源码(实例构造器):

String str = new String();

字节码:

0: new #2 // class java/lang/String

3: dup

4: invokespecial #3 // Method java/lang/String."":()V

7: astore_1

--------------------------------------------------------------------

2、源码(父类方法):

super.func();

字节码:

0: aload_0

1: invokespecial #2 // Method com/Base.func:()V

--------------------------------------------------------------------

3、源码(私有方法):

funcPrivate();

字节码:

0: aload_0

1: invokespecial #2 // Method funPrivate:()V

3.3 步骤三:动态分派(类使用时)

动态分派分为invokevitrual、invokeinterface 与 invokedynamic,其中动态调用invokedynamic是 JDK 1.7 新增的指令,我们单独在另一篇中解析。有些同学可能会觉得方法不重写不就只有一个版本了吗?这个想法忽略了Java动态链接的特性,Java可以从任何途径加载一个class,除非解析的 5 种的情况外,无法保证方法不被重写。

invokevirtual指令

虚拟机为每个类生成虚方法表vtable(virtual method table)的结构,类中声明的方法的入口地址会按固定顺序存放在虚方法表中;虚方法表还会继承父类的虚方法表,顺序与父类保持一致,子类新增的方法按顺序添加到虚方法末尾(这以Java单继承为前提);若子类重写父类方法,则重写方法位置的入口地址修改为子类实现;1)类加载解析阶段:解析类的继承关系,生成类的虚方法表 (包含了这个类型所有方法的入口地址)。举个例子,有Class B继承与Class A,并重写了A中的方法:

Object是所有类的父类,所有每个类的虚方法表头部都会包含Object的虚方法表。另外,B重写了A#printMe(),所以对应位置的入口地址方法被修改为B重写方法的入口地址。

需要注意的是,被final、static或private修饰的方法不会出现在虚方法表中,因为这些方法无法被继承重写。2)调用阶段(动态分派):解析阶段生成虚方法表后,每个方法在虚方法表中的索引是固定的,这是不会随着实际类型变化影响的。调用方法时,首先根据变量的实际类型获得对应的虚方法表(包含了这个类型所有方法的入口地址),然后根据索引找到方法的入口地址。

invokeinterface指令

接口方法的选择行为与类方法的选择行为略有区别,主要原因是Java接口是支持多继承的,就没办法像虚方法表那样直接继承父类的虚方法表。虚拟机提供了itable(interface method table)来支持多接口,itable由偏移量表offset table与方法表method table两部分组成。

当需要调用某个接口方法时,虚拟机会在offset table查找对应的method table,随后在该method table上查找方法。

3.4 性能对比invokestatic & invokespecial可以直接调用方法入口地址,最快

invokevirtual通过编号在vtable中查找方法,次之

invokeinterface现在offset table中查找method table的偏移位置,随后在method table中查找接口方法的实现

4. 总结方法调用的本质是从符号引用转换到直接引用(方法入口地址)的过程,一共需要经过(编译时)生成符号引用、(类加载时)解析、(调用时)动态分派三个步骤

invokestatic & invokespecial指令在(类加载时)解析时根据静态类型完成转换

invokevirtual & invokeinterface在(调用时)根据实际类型,查找vtable & itable完成转换

重载其实是编译器的语法特性与多态无关,对编译时符号引用生成有影响,在运行时已经没有影响了;重写是多态的基础,虚拟机通过vtable & itable来支持虚方法的方法选择。

参考资料《深入理解Java虚拟机(第3版本)》(第8章)—— 周志明 著

《深入理解Android:Java虚拟机 ART》(第2章) —— 邓凡平 著

《深入理解 JVM 字节码》(第2、3章)—— 张亚 著

推荐阅读

感谢喜欢!请点赞,你的点赞和关注真的对我非常重要!欢迎关注彭旭锐的Github!

java invokevirtual_Java | 深入理解方法调用的本质(含重载与重写区别)相关推荐

  1. c++ 重载 重写_Java | 深入理解方法调用的本质(含重载与重写区别)

    前言 对于习惯使用面向对象开发的工程师们来说,重载 & 重写 这两个概念应该不会陌生了.在中 / 低级别面试中,也常常会考察面试者对它们的理解(隐约记得当年在校招面试时遇到过): 网上大多数资 ...

  2. 《Java 核心技术卷1 第10版》学习笔记------ -理解方法调用【重载解析、静态绑定、动态绑定】

    弄清楚如何在对象上应用方法调用非常重要.下面假设要调用 x.f(args,) 隐式参数 x 声明为类 C 的一个对象.下面是调用过程的详细描述: 1 ) 编译器査看对象的声明类型和方法名.假设调用 x ...

  3. Java知识整理——远程方法调用

    什么是RMI ? Java远程方法调用(RMI)是一个Java API,它执行的面向对象的等价远程过程调用(RPC)的方法,包括了直接传输序列化的Java类和分布式垃圾收集的支持. 远程方法调用(RM ...

  4. 使用JavaSymbolSolver解决Java代码中的方法调用

    为什么创建java-symbol-solver? 几年前,我开始使用JavaParser ,然后开始做出贡献. 不久之后,我意识到我们想对Java代码执行的许多操作不能仅通过使用解析器生成的抽象语法树 ...

  5. Java内联虚拟方法调用的性能

    总览 动态编译的好处之一是,它能够支持在虚拟方法代码上进行广泛的方法内联. 虽然内联代码可以提高性能,但是代码仍然必须检查类型(以防由于优化而更改了类型)或在多个可能的实现之间进行选择. 这导致了问题 ...

  6. Java基础+流程控制+方法+数组【笔记含代码】

    文章目录 什么是计算机 计算机硬件 计算机软件 DOS命令 计算机语言发展史 第一代语言 第二代语言 第三代语言 Java帝国的诞生 C & C++ 反抗 Java初生 Java发展 Java ...

  7. Java基础知识之方法的返回值与重载

    文章目录 一.方法的返回值 二.方法的重载 一.方法的返回值 1.说明 就是方法调用结束的标志,会返回一个值给调用该方法的方法里,然后我们常用的就是使用一个变量去接收这个值,并把这个值用作其他的操作. ...

  8. 方法的重载与重写区别

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

  9. java流程控制原理与方法_1.从本质上看,计算机控制系统的工作原理可归纳为三个步骤,以下不属这三个步骤的是 ( )。_学小易找答案...

    [单选题]下列语句序列执行后, i 的值是( ); int i =1 ; switch ( i ) { case 1: i++; case 2: i+=2; } [单选题]对于 while语句而言, ...

  10. java继承链中方法调用优先级.顺序:this.show(object)super.show(object)this.show((super)object)super.show((super))

    先看代码如下: 运行结果 在上述代码中,类B继承了类A,类C继承了类B,A中show()方法发生了重载,B中重写了A中的show(C )方法. 这里先说一下向上造型的一个知识点:向上造型时,编译期时根 ...

最新文章

  1. 面试必备,各种技术知识集大成之项目~
  2. LeetCode Integer to English Words(整数转化为英文字母)
  3. 验证STIL的pattern的输入输出顺序
  4. 最优非对称加密填充(OAEP)
  5. Django中cookie和session的存、取、删除
  6. WPF系列 自定控件
  7. C++11标准之NULL与nullptr比较
  8. bootstrap table的属性sidePagination设置不当导致数据不显示
  9. 解决问题--修改weblogic密码后无法启动以及如何解密weblogic的3DES密文
  10. 好用的论文翻译工具集锦
  11. 计算机函数if使用折扣率,excel1-服装采购表题目要求
  12. java实验作业:银行账户
  13. java能自学吗_java能自学吗?
  14. 利用MATLAB免费生成GIF
  15. 5月9日机构对金融市场观点汇总
  16. Apache htaccess 重写如果文件存在!
  17. pkuseg对文件分词时报错
  18. Qt:QTableView(01) 的用法
  19. 神经网络算法的应用领域,神经元网络算法的思想
  20. 1、野火freertos学习笔记

热门文章

  1. hudson构建配置
  2. Ubuntu下搭建C/C++开发环境
  3. 6迁移-企业级 Hyper-v 群集部署实验方案
  4. winform获取appconfig配置文件得配置
  5. 压缩感知重构算法——SP算法
  6. 代码管理学:通过文档记录,实现工作传承
  7. FreeSwitch的Canvas功能
  8. 解决办法:E: 无法获得锁 /var/lib/apt/lists/lock - open (11: 资源暂时不可用)
  9. NPAPI插件无法加载,有可能跟JDK相关
  10. JAVA interface报错:abstract methods do not specify a body