方法调用:一看就懂,一问就懵?

前端 2023-07-05 17:29:38
57阅读

方式启用是否很了解?那么你确实掌握它吗?今日就要大家来盘一下它。

最先大伙儿要确立一个定义,这里的方式启用并并不是方式中的编码强制执行,只是要明确被启用方式的版本号,即最后会启用哪一个方式。

上一篇文章中大家掌握到,class字节码文档中的方式的启用都仅仅标记引入,而不是直接引用(方式在具体运作时运行内存合理布局中的通道详细地址),要完成二者的转换,就迫不得已提及分析和分配了。

分析

大家以前说过在类载入的分析环节,会将一部分的标记引入转换为直接引用,该分析创立的前提条件是:方式在程序流程真真正正运作以前就早已有一个可明确的启用版本号,而且这一方式的启用版本号在运作期是不能更改的。大家把这类方法的启用称之为分析(Resolution)。

见到这一必要条件,是否有小伙伴们想到到目标的泛素化?照片没有错,就这样,在java中能达到不被调用的方式有静态方法、独享方式(不可以被外界浏览)、案例构造器和被final装饰的方式,因而他们都合适在类载入环节开展分析,此外根据this或是super启用的父类方法也是在类载入环节开展分析的。

指令系统

启用不一样种类的方式,字节码指令系统里设定了不一样的命令,在jvm里边出示了5条方式启用字节码命令:

  • invokestatic:启用静态方法,分析环节明确唯一方式版本号
  • invokespecial:案例构造器init方式、独享及父类方法,分析环节明确唯一方式版本号
  • invokevirtual:启用全部虚方式
  • invokeinterface:启用插口方式,在运作时再明确一个完成该插口的目标

invokedynamic:先在运作时动态性分析出启用点限定符所引入的方式,随后再实行该方式,在此之前的4条启用命令,分配逻辑性是干固在Javavm虚拟机內部的,而invokedynamic命令的分配逻辑性是由客户所设置的正确引导方式决策的。

invokedynamic命令是Java7中提升的,是为完成动态性种类的语言表达做的一种改善,可是在java7中并沒有立即出示转化成该命令的方式,必须依靠ASM最底层字节码专用工具来造成命令,直至java8的lambda关系式的发生,该命令才拥有立即的生成方式。

「小知识要点:静态数据种类语言表达与动态性种类语言表达」

他们的差别就取决于对种类的查验是在编译程序期或是在运作期,达到前面一种便是静态数据种类语言表达,相反是动态性种类语言表达。即静态数据种类语言表达是分辨自变量本身的类型信息,动态性种类语言表达是分辨变量类型的类型信息,自变量沒有类型信息,变量类型才有类型信息,它是动态语言的一个关键特点。

「例」java类中界定的基本上基本数据类型,在申明时就早已明确了他的实际种类了;而JS选用var来界定种类,值是啥种类便会在启用时应用哪些种类。

虚方式和非虚方式

字节码指令系统为invokestatic、invokespecial或是是用final装饰的invokevirtual的方式得话,都能够在分析环节中明确唯一的启用版本号,合乎这一标准的便是大家上面提及的五类方式。他们在类载入的情况下便会把标记引入分析为该方式的直接引用,这种方式能够称之为「非虚方式」。与之反过来,并不是非虚方式的方式是「虚方式」。照片

分配

如果我们在编译程序期内沒有将方式的标记引入转换为直接引用,只是在运作期内依据方式的具体种类关联有关的方式,大家把这类方式的启用称之为分配。在其中分配又分成静态数据分配和动态性分配。

静态数据分配

不清楚你对轻载掌握是多少?为了更好地表述静态数据分配,大家先再来一个轻载的趣味测试:

 
  1. public class StaticDispatch { 
  2.      
  3.     static abstract class Human { 
  4.     } 
  5.  
  6.     static class Man extends Human { 
  7.     } 
  8.  
  9.     static class Woman extends Human { 
  10.     } 
  11.  
  12.     public void sayHello(Human guy) { 
  13.         System.out.println("hello,guy!"); 
  14.     } 
  15.  
  16.     public void sayHello(Man guy) { 
  17.         System.out.println("hello,gentleman!"); 
  18.     } 
  19.  
  20.     public void sayHello(Woman guy) { 
  21.         System.out.println("hello,lady!"); 
  22.     } 
  23.  
  24.     public static void main(String[] args) { 
  25.         Human man = new Man(); 
  26.         Human woman = new Woman(); 
  27.         StaticDispatch sr = new StaticDispatch(); 
  28.         sr.sayHello(man); 
  29.         sr.sayHello(woman); 
  30.     } 
 
  1. hello,guy! 
  2. hello,guy! 

你答对了嘛?最先大家来掌握2个定义:静态数据种类和具体种类。拿Human man = new Man();而言Human称之为自变量的静态数据种类,而Man大家称之为自变量的具体种类,差别以下:

  1. 静态数据种类的转变只是在应用时才产生,自变量自身的静态数据种类是不容易被更改,而且最后静态数据种类在编译程序期是得知的。
  2. 具体种类的转变是在运作期才知道,c语言编译器在编译程序时并不了解一个目标的实际种类是啥。

这里往往实行的是Human种类的方式,是由于c语言编译器在轻载时,会根据主要参数的「静态数据种类」来做为判断实行方式的根据,而不是应用「具体种类」。

全部依靠静态数据种类来精准定位方式实行版本号的分配姿势称之为静态数据分配。静态数据分配的典型性运用便是方法重载。静态数据分配产生在编译程序环节,因而明确静态数据分配的姿势事实上并不是由vm虚拟机来实行的,只是由c语言编译器来进行。

动态性分配

了解了轻载以后再说掌握下调用?实例走一走:

 
  1. public class DynamicDispatch { 
  2.  
  3.     static abstract class Human{ 
  4.         protected abstract void sayHello(); 
  5.     } 
  6.      
  7.     static class Man extends Human{ 
  8.         @Override 
  9.         protected void sayHello() { 
  10.             System.out.println("man say hello!"); 
  11.         } 
  12.     } 
  13.     static class Woman extends Human{ 
  14.         @Override 
  15.         protected void sayHello() { 
  16.             System.out.println("woman say hello!"); 
  17.         } 
  18.     } 
  19.     public static void main(String[] args) { 
  20.  
  21.         Human man = new Man(); 
  22.         Human woman = new Woman(); 
  23.         man.sayHello(); 
  24.         woman.sayHello(); 
  25.         man = new Woman(); 
  26.         man.sayHello(); 
  27.     } 
  28.  

请考虑一下輸出結果,再次缄默2分钟。回答是:

 
  1. man say hello! 
  2. woman say hello! 
  3. woman say hello! 

此次坚信大伙儿的結果都正确了吧?大家先来填补一个知识要点:

父类引入偏向派生类时,假如实行的父类方法在派生类中未被调用,则启用本身的方式;假如褥子类调用了,则启用派生类的方式。假如要应用派生类独有的特性和方式,必须往下转型发展。

依据这一结果大家反方向逻辑推理一下:man和women是静态数据种类同样的自变量,他们在启用同样的方式sayHello()时回到了不一样的結果,而且在自变量man的2次启用中实行了不一样的方式。造成 这一状况的缘故很显著,是这两个自变量的「具体种类」不一样,Javavm虚拟机是怎样依据具体种类来分配方式实行版本号的呢?大家看下字节码文档:

 
  1. man.sayHello(); 
  2. woman.sayHello(); 

大家关心的是之上二行编码,她们相匹配的分别是17和21行的字节码命令。单单从字节码命令视角看来,它俩的命令invokevirtual和变量定义$Human.sayHello:()V是彻底一样的,可是实行的結果确是不一样的,因此 大家得科学研究下invokevirtual命令了,操作步骤以下:照片

  1. 寻找操作数栈顶的第一个原素所偏向的目标的具体种类,记作C。
  2. 假如在种类C中寻找与变量定义中的描述符和简易名字都相符合的方式,则开展访问限制校检,假如根据则回到这一方式的直接引用,搜索全过程完毕;假如不通过,则回到java.lang.IllegalAccessError出现异常(倘若没有一同一个jar包下便会报非法访问出现异常)。
  3. 不然,依照承继关联从下往上先后对C的每个父类开展第二步的检索和认证全过程。
  4. 假如自始至终沒有寻找适合的方式,则抛出去java.lang.AbstractMethodError出现异常。

因为invokevirtual命令实行的第一步便是在运作期明确接受者的具体种类,因此 2次启用中的invokevirtual命令并并不是把常量池中方式的标记引入分析到直接引用上就结束了,还会继续依据接受者的具体种类来挑选方式版本号(实例中的具体种类为Man和Woman),这一全过程便是Java语言表达中方式调用的「实质」。

大家把这类在运作期依据具体种类明确方式实行版本号的分配全过程称之为动态性分配。

单分配与多分配

方式的接受者与方式的主要参数通称为方式的宗量,这一界定最开始应当来自《Java与模式》一书。依据分配根据是多少种宗量,能够将分配区划为单分配和多分配二种。单分配是依据一个宗量对总体目标方式开展挑选,多分配则是依据超过一个宗量对总体目标方式开展挑选。

「举例子」

 
  1. public class Dispatch{ 
  2.     static class QQ{} 
  3.     static class_360{} 
  4.      
  5.     public static class Father{ 
  6.         public void hardChoice(QQ arg){ 
  7.             System.out.println("father choose qq"); 
  8.         } 
  9.         public void hardChoice(_360 arg){ 
  10.             System.out.println("father choose 360"); 
  11.         } 
  12.     } 
  13.     public static class Son extends Father{ 
  14.         public void hardChoice(QQ arg){ 
  15.             System.out.println("son choose qq"); 
  16.         } 
  17.         public void hardChoice(_360 arg){ 
  18.             System.out.println("son choose 360"); 
  19.         } 
  20.     } 
  21.     public static void main(String[]args){ 
  22.         Father father=new Father(); 
  23.         Father son=new Son(); 
  24.         father.hardChoice(new_360()); 
  25.         son.hardChoice(new QQ()); 
  26.     } 

请考虑一下輸出結果,再次缄默2分钟。回答是:

 
  1. father choose 360 
  2. son choose qq 

大家讨论一下编译程序环节c语言编译器的挑选全过程,也就是静态数据分配的全过程。这时候挑选总体目标方式的根据有二点:一是静态数据种类是Father或是Son,二是方式主要参数是QQ或是360。此次挑选結果的最后物质是造成了两根invokevirtual命令,两根命令的主要参数各自为常量池中偏向Father.hardChoice(360)及Father.hardChoice(QQ)方式的标记引入。由于是依据2个宗量开展挑选,因此 Java语言表达的静态数据分配归属于多分配种类。

再看一下运作环节vm虚拟机的挑选,也就是动态性分配的全过程。在实行“son.hardChoice(new QQ())”这句话编码时,更精确地说,是在实行这句话编码所相匹配的invokevirtual命令时,因为编译程序期早已决策总体目标方式的签字务必为hardChoice(QQ),vm虚拟机这时不容易关注传送回来的主要参数“QQ”到底是“腾讯官方QQ”或是“奇瑞QQ”,由于这时候主要参数的静态数据种类、具体种类都对方式的挑选不容易组成一切危害,唯一能够危害vm虚拟机挑选的要素仅有此方式的接受者的具体种类是Father或是Son。由于只有一个宗量做为挑选根据,因此 Java语言表达的动态性分配归属于单分配种类。

虚方式表

在面向对象编程的程序编写中,会很经常的应用到动态性分配,假如在每一次动态性分配的全过程上都要再次在类的方式数据库中检索适合的总体目标得话就很可能危害到实行高效率。因而,为了更好地提升 特性,jvm选用在类的方法区创建一个虚方式表(Vritual Method Table,也称之为vtable,与此对应的,在invokeinterface实行时也会采用插口方式表——Inteface Method Table,通称itable)来完成,应用虚方式表数据库索引来替代数据库搜索以提升 特性。

每一个类上都有一个虚方式表,表格中储放着各种各样方式的具体通道:

  • 假如某一方式在派生类中沒有被调用,那派生类的虚方式表里边的详细地址通道和父类同样方式的详细地址通道是一致的,都偏向父类的完成通道。
  • 假如派生类中调用了这一方式,派生类方式表格中的详细地址可能更换为偏向派生类完成版本号的通道详细地址。

Son调用了来源于Father的所有方式,因而Son的方式表沒有偏向Father种类数据信息的箭头符号。可是Son和Father也没有调用来源于Object的方式,因此 他们的方式表格中全部从Object承继来的方式都偏向了Object的基本数据类型。

为了更好地程序代码上的便捷,具备同样签字的方式,在父类、派生类的虚方式表上都理应具备一样的数据库索引编号,那样当种类转换时,仅必须变动搜索的方式表,就可以从不一样的虚方式表格中按数据库索引变换出所需的通道详细地址。方式表一般在类载入的联接环节开展复位,提前准备了类的自变量初值后,虚似机遇把此类的方式表也复位结束。

关联体制

分析启用一定是个静态数据的全过程,在编译程序期内就彻底明确,在类装车的分析环节便会把涉及到的标记引入所有变化为可明确的直接引用,不容易延迟时间到运作期再去进行。分配(Dispatch)启用则可能是静态数据的也可能是动态性的。因而大家把 「分析」 和 「静态数据分配」 这个在编译程序期内就明确了被启用的方式,且在运作期内不会改变的启用称作静态数据连接,而在运作期才明确出来启用方式的称作动态链接。

大家把在静态数据连接全过程中的变换变成初期关联,将动态链接全过程中的变换称作末期关联。

见到这,方式的启用你弄懂了没有?假如你有没有什么疑惑得话,能够关心微信公众平台“阿Q正传说编码”,还可以加阿Q正传朋友qingqing-4132,阿Q正传随时欢迎的来临!

the end
免责声明:本文不代表本站的观点和立场,如有侵权请联系本站删除!本站仅提供信息存储空间服务。