当前位置: 首页 > 科技观察

方法调用:一看就懂,一问就糊涂?你熟悉

时间:2023-03-16 18:15:50 科技观察

方法调用吗?你真的了解吗?今天就让我们一起来看看吧。首先大家要明确一个概念。这里的方法调用并不是正在执行的方法中的代码,而是被调用方法的版本,即最后会调用哪个方法。在上一篇文章中,我们了解到类字节码文件中的方法调用只是符号引用,而不是直接引用(方法实际运行时在内存布局中的入口地址)。要实现两者之间的转换,就更不用说解析和调度了。分析我们之前说过,在类加载的分析阶段,会将一些符号引用转化为直接引用。这个分析的前提是方法在程序真正运行之前有一个可确定的调用版本,而方法的调用版本在运行时是不可变的。我们称这种类型的方法调用为Resolution。看到这个前提,有没有人想到对象的多态性?图片是对的,就是这样。在Java中,可以满足非重写要求的方法包括静态方法、私有方法(不能被外部访问)、实例构造函数和final修饰的方法,因此适合在类加载阶段进行解析,而this或super调用的父类方法也在类加载阶段被解析。指令集调用不同类型的方法。字节码指令集中设置了不同的指令。jvm中有5条方法调用字节码指令:invokestatic:调用一个静态方法,在解析阶段确定唯一方法版本invokespecial:实例构造Init方法,私有和父类方法,唯一方法版本在解析阶段invokevirtual:调用所有虚方法invokeinterface:调用接口方法,然后在运行时确定一个实现该接口的对象invokedynamic:在运行时首先动态解析出调用点限定符所引用的方法,然后执行该方法。前面4条call指令的调度逻辑固化在Java虚拟机内部,invokedynamic指令的调度逻辑由用户设置的boot方法决定。.Java7中加入了invokedynamic指令,是对动态类型语言实现的改进,但是Java7中没有直接生成这条指令的方法,需要借助ASM底层字节码工具生成指令,直到Java8有了lambda表达式的出现,这条指令有了直接生成的方法。《小知识点:静态类型语言和动态类型语言》它们的区别在于类型检查是在编译时还是在运行时进行。如果满足前者,就是静态类型语言,反之,就是动态类型语言。也就是说,静态类型语言是判断变量本身的类型信息,动态类型语言是判断变量值的类型信息。变量没有类型信息,但变量值有类型信息。这是动态语言的一个重要特征。【例】java类中定义的基本数据类型,在声明的时候就已经确定了它的具体类型;而在JS中,var是用来定义类型的,调用的时候会用到value的类型。如果虚方法和非虚方法的字节码指令集是invokestatic、invokespecial或者final修饰的invokevirtual的方法,在解析阶段就可以确定唯一的调用版本。满足这个条件的五种,就是我们上面说的五类。方法。当加载类时,它们会将符号引用解析为对该方法的直接引用。这些方法可以称为“非虚拟方法”。相反,不是非虚拟的方法是“虚拟方法”。Imagedispatch如果我们在编译时不将方法的符号引用转换为直接引用,而是在运行时根据方法的实际类型绑定相关方法,我们称这种方法调用为dispatch。分配又分为静态分配和动态分配。静态调度。我不知道你对超载了解多少。为了解释静态调度,让我们来做一个重载的小测试:System.out.println("你好,先生!");}publicvoidsayHello(Womanguy){System.out.println("你好,女士!");}publicstaticvoidmain(String[]args){Humanman=newMan();Humanwoman=newWoman();StaticDispatchsr=newStaticDispatch();sr.sayHello(man);sr.sayHello(woman);}}你好,伙计!小伙儿,你好!你做对了吗?首先,让我们了解两个概念:静态类型和实际类型。以人类man=newMan();例如,Human称为变量的静态类型,而Man称为变量的实际类型,区别如下:静态类型的变化只在使用时发生,而变量的静态类型本身不会改变,最终的静态类型在编译时是已知的。实际类型的变化只有在运行时才知道,编译器在编译程序时并不知道一个对象的具体类型是什么。这里之所以执行Human类型的方法,是因为编译器在重载时,会以参数的“静态类型”作为判断执行方式的依据,而不是使用“实际类型”。所有依赖静态类型来定位方法实现的调度动作都称为静态调度。静态分派的典型应用是方法重载。静态分配发生在编译期间,因此决定静态分配的行为实际上并不是由虚拟机执行的,而是由编译器执行的。了解了dynamicdispatch的重载,我们来学习rewriting?案例开始:");}}publicstaticvoidmain(String[]args){Humanman=newMan();Humanwoman=newWoman();man.sayHello();woman.sayHello();man=newWoman();man.sayHello();}}请考虑输出并继续沉默两分钟。答案是:mansayhello!女人打招呼!女人打招呼!这次相信大家的结果是正确的吧?先补充一个知识点:当父类引用指向子类时,如果执行的父类方法没有在子类中重写,则调用自己的方法;如果被子类覆盖,则调用子类的方法。如果你想使用子类特有的属性和方法,你需要向下转型。根据这个结论,我们倒过来推理一下:man和women是同一个静态类型的变量,他们在调用同一个方法sayHello()时返回不同的结果,在变量man的两次调用中执行不同的方法。出现这种现象的原因是显而易见的。这两个变量的“实际类型”是不同的。Java虚拟机如何根据实际类型分配方法的执行版本?让我们看一下字节码文件:man.sayHello();女人.sayHello();我们重点关注上面两行代码,分别对应第17行和21行的字节码指令。从字节码指令来看,指令invokevirtual和常量$Human.sayHello:()V是一模一样的,但是执行结果确实不一样,所以我们要研究invokevirtual指令,运行过程如下:找到操作数栈顶的第一个元素指向的对象的实际类型,记为C。如果在类型C中找到了同时匹配常量中的描述符和简单名称的方法,则访问权限检查为执行,如果通过则返回该方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常(如果不在同一个jar包中,会报非法访问异常)。否则,根据自下而上的继承关系,对C的各个父类进行第二步的查找验证过程,如果没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。由于执行invokevirtual指令的第一步是在运行时判断接收者的实际类型,因此两次调用中的invokevirtual指令并没有以将常量池中方法的符号引用解析为直接引用而结束,还要根据接收到的用户的实际类型来选择方法版本(案例中的实际类型是Man和Woman)。这个过程就是Java语言中方法重写的“精髓”。这种在运行时根据实际类型确定方法执行版本的调度过程,我们称之为动态调度。single-dispatch和multi-dispatch方法的接收者和参数统称为方法参数。这个定义应该是出自《Java与模式》一书。根据赋值的类型多少,赋值可以分为单一赋值和多重赋值。Single-dispatch是根据一个数量来选择目标方法,multi-dispatch是根据一个以上的数量来选择目标方法。“示例”publicclassDispatch{staticclassQQ{}staticclass_360{}publicstaticclassFather{publicvoidhardChoice(QQarg){System.out.println("fatherchooseqq");}publicvoidhardChoice(_360arg){System.out.println("fatherchoose360");}}publicstaticclassSonextendsFather{publicvoidhardChoice(QQarg){System.out.println("sonchooseqq");}publicvoidhardChoice(_?arg){System.out.println("sonchoose360");}}publicstaticvoidmain(String[]args){fatherfather=newFather();父亲儿子=新儿子();father.hardChoice(new_360());son.hardChoice(newQQ());}}考虑输出结果,沉默持续了两分钟。答案是:fatherchoose360sonchooseqq我们来看看编译器在编译阶段的选择过程,也就是静态分配的过程。这时候选择目标方法有两个依据:一是静态类型是Father还是Son,二是方法参数是QQ还是360。这个选择结果的最终产物是两条invokevirtual指令。两条指令的参数都是对常量池中的Father.hardChoice(360)和Father.hardChoice(QQ)方法的符号引用。因为选择是基于两个参数的,所以Java语言的静态调度属于多调度类型。再来看运行阶段虚拟机的选择,也就是动态分配的过程。在执行代码“son.hardChoice(newQQ())”时,更准确地说,在执行这段代码对应的invokevirtual指令时,目标方法的签名必须是hardChoice(QQ),虚拟机不会关心传递的参数“QQ”为“腾讯QQ”或“奇瑞QQ”,因为此时参数的静态类型和实际类型不会对方法选择产生任何影响。唯一能影响虚拟机选择的因素是这个方法的实际接受者类型是父还是子。由于只有一个数量作为选择的依据,所以Java语言的动态调度属于单一调度类型。在面向对象编程中,虚方法表经常用于动态调度。如果在每次动态派发过程中都要在类的方法元数据中重新寻找合适的目标,很可能会影响执行效率。.因此,为了提高性能,jvm在类的方法区采用了虚方法表(VirtualMethodTable,也称vtable)。相应的,在执行invokeinterface时也会用到接口方法表-IntefaceMethodTable。简称itable)来实现,使用虚方法表索引代替元数据查找来提高性能。每个类都有一个虚方法表,里面存放着各种方法的实际表项:如果子类中没有重写过某个方法,则子类的虚方法表中的地址表项与父类的地址表项相同方法的入口是一致的,指向父类的实现入口。如果在子类中重写了该方法,则子类方法表中的地址将被替换为指向子类实现版本的入口地址。Son覆盖了Father的所有方法,所以Son的方法表中没有指向Father类型数据的箭头。但是Son和Father都没有重写Object的方法,所以他们的方法表中所有继承自Object的方法都指向Object的数据类型。为了程序实现的方便,相同签名的方法在父类和子类的虚方法表中应该有相同的索引号,这样改变类型时只需要改变要查找的方法表即可,并且可以从不同的方法中获得该方法。根据虚方法表中的索引转换需要的入口地址。方法表一般在类加载的连接阶段初始化。类的变量初值准备好后,虚拟机还要初始化类的方法表。绑定机制的解析和调用必须是一个静态过程,完全在编译期确定。在类加载的解析阶段,所有涉及到的符号引用都会被转换为可确定的直接引用,不会延迟到运行时才完成。调度呼叫可以是静态的或动态的。因此,我们把在编译时调用的方法称为“解析”和“静态调度”,在运行时不改变的调用称为静态链接,在运行时调用的方法称为动态链接。我们将静态链接期间的转换称为早期绑定,将动态链接期间的转换称为后期绑定。看到这里,方法调用你懂了吗?如果你还在迷茫,可以关注微信公众号“阿Q说密码”,也可以加阿Q好友qingqing-4132,阿Q期待你的到来!本文转载自微信公众号“阿Q说码”,可通过以下二维码关注。转载本文请联系阿Q并说出密码公众号。