第19章 方法的调用与返回

Posted mongotea

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第19章 方法的调用与返回相关的知识,希望对你有一定的参考价值。

Java虚拟机的指令集包括四种调用方法的指令,本章对这四种指令和这些指令执行的环境进 行阐述。

19.1方法调用

Java程序设计语言提供了两种基本的方法:实例方法和类(或者静态)方法。这两种方法的区别在于:

1)实例方法在被调用之前,需要一个实例,而类方法不需要。

2)实例方法使用动态(迟)绑定,而类方法使用静态(早)绑定。

当Java虚拟机调用一个类方法时,它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类(只能在运行时得知)来选择所调用的方法。

Java虚拟机使用两种不同的指令分别调用这两种方法。对于实例方法,使用invokevirtual指令;对于类方法,使用invokestatic指令。这两种指令如表19-1所示。
表19-1 方法调用
操作码 操作数 说 明
invokevirtual indexbyte1,indexbyte2 把objectref (对象引用)和args (参数) 从栈中弹出,调用常量池索引指向的实例方法
invokestatic indexbyte1,indexbyte2 把args从栈中弹出,调用常量池索引指向的类方法

如前所述,到方法的引用最初是符号化的。所有的调用指令(例如invokevirtual和 invokestatic )都指向一个最初包含符号引用的常量池人口。当Java虚拟机遇到一条调用指令时, 如果还没有解析符号引用,那么虚拟机把解析符号引用作为执行指令调用执行过程中的一部分。

要解析一个符号引用,Java虚拟机要确定被符号化引用的方法,然后再用一个直接引用来代 替符号引用。直接引用就如同偏移量指针一样,如果将来再次使用该引用,它可以使虚拟机更 快地调用这个方法。

例如,在执行invokevirtual指令之前,Java虚拟机使用紧随invokevirtual操作码之后的 indexbytel和indexbyte2这两个操作数,产生一个指向当前类常量池的无符号16位索引。这个常 量池人口包含一个指向将要调用的方法的符号引用。

在解析过程中,Java虚拟机还将执行几次确认检验,这些检验能够确保遵循了java语言规范, 能够确保调用invoke指令的安全。比方说,虚拟机首先确认被符号化引用的方法是否存在,如果 存在,虚拟机再检验当前类是否能够合法地访问这个方法。比方说,如果这个方法为私有方法, 那么它必须为当前类的成员。上述任何一个检验失败,Java虚拟机都会抛出异常。第5章对这个解析过程进行了概述。第8章对这个过程进行了详细描述。

一旦解析了一个方法后,Java虚拟机就准备调用它。如果这个方法是一个实例方法,它必须 在一个对象中被调用。对每一次实例方法的调用,虚拟机需要在栈里存在一个对象引用 (objectref)。如果该方法需要参数,那么除了objectref,虚拟机还需要在栈中存在该方法所需要 的参数(args)。如果这个方法是一个类方法,虚拟机不冉需要objectref,因为虚拟机不会在对 象上调用一个类方法,栈中存在的将只有args。

objectref和args (或者在类方法的倩况下只有args)必须在调用指令执行前,被其他指令压 入所调用方法的操作数栈。

19.1.1 Java方法的调用

如第5章所述,虚拟机为每一个调用的Java (非本地)方法建立一个新的桟帧。栈帧包括: 为方法的局部变量所预留的空间,该方法的操作数栈,以及特定虚拟机实现需要的其他所有的信息。局部变量和操作数栈的大小在编译时计算出来,并放置到class文件中去,然后虚拟机就能够了解到方法的栈帧需要多少内存。当虚拟机调用一个方法的时候,它为该方法创建恰当大小的栈帧,再将新的栈帧压入Java栈。


处理实例方法时,虚拟机从所调用方法栈帧内的操作数栈中弹出objectref和args。虚拟机把 objectref作为局部变量0放到新的栈帧中,把所有的args作为局部变童1,2,……等处理。 objectref是隐式传给所有实例方法的this指针。

对于类方法,虚拟机只从所调用方法栈帧中的操作数栈中弹出参数,并将它们放到新的栈帧中去作为局部变量0,1,2,……

当objectref和args (对于类方法则只有args )被賦给新栈帧中的局部变量之后,虚拟机把新 的栈帧作为当前栈帧,然后将程序计数器指向新方法的第一条指令。

19.1.2本地方法的调用

如第5章所述,虚拟机使用一种“与实现相关”的风格调用本地方法。当调用本地方法时, 虚拟机不会将一个新的栈帧压人Java栈。当线程进入到本地方法的那一刻,它就将Java栈抛在身 后。直到本地方法返回以后,Java栈才被重新使用。

19.2方法调用的其他形式
尽管通常使用invokevirtual指令调用实例方法,但在某些特定的情况中,也会使用另外两种操作码-invokespecial和invokeinterface,如表 19-2所示。

当根据引用的类型来调用实例方法,而不是根据对象的类来调用的时候,通常使用 invokespecial指令。这又分三种情况:

1)实例初始化(<init>())方法。

2)私有方法。

3)使用super关键字所调用的方法。

当给出一个接口的引用时,使用invokeinterface来调用一个实例方法。
如第7章所述,Java虚拟机总是直接调用类初始化(或者<clinit> ())方法,类的初始化方法永远不会被任何字节码调用。在Java虚拟机指令集中,没有任何调用<clinit> ()方法的指令。 如果class文件尝试使用本章所介绍的四种指令来调用<clinit>()方法,会导致虚拟机抛出异常。

19.3 指令invokespecial

invokespecial和invokevirtual的主要区别在于:invokespecial通常(只有一个例外)根据引用 的类型选择方法,而不是根据对象的类来选择。换句话说,它使用静态绑定而不是动态绑定。 在下列使用invokespecial的三种情况中,动态绑定并不会产生所期望的结果。

19.3.1 指令invokespecial 和 <init> ()方法

如第7章所述,<init> ()方法(或者实例初始化方法)是编译器为构造方法和实例变量初 始化方法放置代码的地方。类会为源文件中每个构造方法提供一个<init> ()方法。如果没有在 源文件中显式声明一个构造方法,编译器就会为这个类产生一个默认的无参数的构造方法。这个默认构造方法通常以class文件中的一个<init> ()方法结束。因此,就像每个类都至少会有一个构造方法一样,每个类都至少会有一个<init> ()方法,这些方法通常使用invokespecial调用。

只有创建一个新的实例的时候,才调用<init> ()方法。新创建对象的继承路径中,每个类都至少会调用一个<init>()方法,而且继承路径中的任何一个类都有可能调用多个<init>()方法。

使用invokespecial来调用<init> ()的原因在于:子类的<init> ()方法需要拥有调用超类的 <init> ()方法的能力。这就揭示了为什么一个对象实例化时,有多个<init>()方法被调用的 原因。虚拟机调用对象类中声明过的 <init> ()方法,这个<init> ()方法首先调用同一个类中 的其他<init> ()方法或者超类中的<init> ()方法,这个过程贯穿于对象的整个生命周期。

例如,考虑下列代码:
// On CD-ROM in file invoke/ex1/Dog.java
class Dog {
}

// On CD-ROM in file invoke/ex1/CockerSpaniel.java
class CockerSpaniel extends Dog {

public static void main(String args[]) {
CockerSpaniel bootsie = new CockerSpaniel();
}
}
当调用main ()方法的时候,虚拟机会为新的CockerSpaniel对象分配空间,然后调用 CockerSpaniel的默认无参的<init> ()方法来初始化那段空间。那个方法将会首先调用Dog的 <init> ()方法,然后再依次调用Object的<init> ()方法。类CockerSpaniel的main ()方法的 字节码如下:

// Create a new CockerSpaniel object,push ref
0 new #1 <Class CockerSpaniel>
// Invoke <init>() method on object ref
// new CockerSpaniel();
3 invokespecial #3 <Method <init>() of class CockerSpaniel>
// Note compiler didn‘t store resulting ref in a var
// representing bootaie, because it was never used
6 return // return void from main()

CockerSpaniel类的main ()方法为新的CockerSpaniel对象分配内存,并使用new指令将分配 的内存初始化为默认的初始化值(“#1”指出了需要实例化的类的常量池入口,这里特指CockerSpaniel类)。new指令把一个指向CockerSpaniel对象的引用压入栈。然后main ()方法使 用invokespecial指令通过该对象引用调用类CockerSpaniel的<init> ()方法(“#3”指出了常量池 入口,其中包含了对CockerSpaniel的<init> ()方法的引用)。Java虚拟机把一个新的栈帧压入 Java栈,然后把对象引用赋给新栈帧中的局部变量0。类CockerSpaniel的<init> ()方法的字节码如下所示:


0 aload_0 // Push object ref from local var 1
// Invoke Dog‘s <init>() on object ref
1 invokespecial $4 <Method <init>() of class Dog>
4 return // return void from CockerSpaniel‘s <init>()
如前所述,这里的<init>()方法相当于编译器为类CockerSpaniel自动产生的默认无参数构 造方法。这个方法首先把从局部变量0中取出的已被初始化的对象引用压入栈,然后通过这个引 用调用Dog的<init> ()方法(“#4”指出了常量池入口,其中包含了指向Dog的<init> ()方法 的引用)。Dog类的<init> ()方法的字节码如下所示:

0 aload_0 // Push obj ref from local var 0
// Invoke Object‘s <init>() method on obj ref
1 invokespecial #3 <Method <init>() of class java.lang.Object>
4 return // return void from Dog‘s <init>()
这里的<init> ()方法相当于编译器为类Dog自动产生的默认无参数构造方法。这个方法首 先把从局部变量0中取出的已被初始化的对象的引用压入栈,然后通过这个引用调用Object的 <init>()方法(这里的“#3”指出了常量池人口,其中包含指向Object的<init> ()方法的引用。

这个常量池并不是指向类CockerSpaniel的方法的那个常量池,每个类都有自己的常量池)。当这 三个<init>方法都返回后,由main ()新建的CockerSpaniel对象才完成了初始化工作。

由于每个类至少有一个<init> ()方法,类的<init> ()方法拥有相同的特征签名是很普遍 的现象(方法的特征签名是指它的名宇、参数的数量和类型)。例如,在CockerSpaniel类继承路 径中的下列三个类的<init> ()方法,它们的特征签名相同。CockerSpaniel、Dog和Object都有 —个名为<init> ()的无参数方法。

invokevirtual指令会执行动态绑定和调用CockerSpaniei的<init> ()方法,所以从 CockerSpaniel的<init> ()方法使用invokevirtual指令调用Dog的<init> ()方法是不可能的。然而通过使用invokespecial指令,CockerSpaniel的<init> ()方法能够调用Dog类的<init>方法,因为放在CockerSpaniel的dass文件(常量池人口#4)中的引用的类型为Dog。

19.3.2指令invokespecial和私有方法

当处理私有实例方法时,必须允许子类使用与超类中实例方法同样的特征签名来声明实例方法(invokespecial只用来调用私有实例方法,不能调用私有类方法,私有类方法由invokestatic指令调用)。例如,在下列代码中,interestingMethod ()是超类中的私有方法,子类对其具有包访问权限。
// On CD-ROM in file invoke/ex2/Superclass.java
class Superclass {

private void interestingMethod() {
System.out.println("Superclass‘s interesting method.");
}

void exampleMethod() {
interestingMethod();
}
}

// On CD-ROM in file invoke/ex2/Subclass.java
class Subclass extends Superclass {

void interestingMethod() {
System.out.println("Subclass‘s interesting method.");
}

public static void main(String args[]) {
Subclass me = new Subclass();
me.exampleMethod();
}
}

如前所述,当调用像前面所定义的Subclass中的main ()方法时,虚拟机会实例化一个新的 Subclass对象,然后调用exampIeMethod {)方法。类Subclass的main ()方法的字节码如下所示:
// Create a new instance of class Subclass, push ref
0 new #2 <Class Subclass>

3 dup // Duplicate ref, push duplicate
// Invoke <init>() method on new object:
// Subclass me=new Subclass();
4 invokespecial #6 <Method <init>() of class Subclass>
7 astore_l // Pop object ref into local var 1
8 aload_l // Push ref from local var 1
// Invoke exampIeMethod() on object ref:
// me.exampIeMethod();
9 invokevirtual #8 <Method void exampIeMethod() of class Superclass>
12 return // return void from main()

Subclass从Superclass处继承了exampleMethod ()方法。当方法main ()调用Subclass对象中的方法exampleMethod ()时,它使用invokevirtual指令。正如类Superclass中所定义的,Java 虚拟机将会创建一个新的栈帧并将其压人栈,然后开始执行exampleMethod ()的字节码。方法 exampleMethod ()的字节码如下所示:

0 aload_0 // Push obj ref from local var 1
// Invoke interestingMethod() on obj ref;
// interestingMetbod();
1 invokespecial #7 <Method void interestingMethod() of Superclass>
4 return //return void from exampleMethod ()

需要注意的是,方法exampleMethod ()首先将赋给局部变量0的引用压人栈(隐含参数this 被传给所有的实例方法),然后使用invokespecial指令通过这个引用调用interestingMethod ()方 法。尽管这里的对象是Subclass类的实例,而且Subclass类中的interestingMethod ()方法也是能 够访问的,但Java虚拟机最终还是调用了Superclass类中的interestingMethod ()方法。

程序的正确输出为:“Superclass’s interesting method’。如果使用指令invokevirtual而不是指令invokespecial来调用方法interestingMethod (),那么程序的输出结果会是:“SubClass‘s interesting method”。这是因为,虚拟机将会根据对象实际所属的类来选择调用哪一个 interestingMethod (),这里对象实际所属的类是Subclass,因此,虚拟机将会使用Subclass的 interestingMethod (相反,当使用invokespecial指令时,虚拟机会按照引用的类型来选择调用的方法,因此就会调用Superclass的interestingMethod()版本。

19.3.3 指令invokespecial和super关键字

当使用super关键字来调用方法时(例如super.someMethod ()),尽管当前类重载了该方法, 但使用者真正希望调用的是超类的方法。再说一次,指令invokevirtual只能调用当前类的方法, 无法使用超类的方法。例如,考虑如下代码:
// On CD-ROM in file invoke/ex3/Cat.java
class Cat {

void someMethod() {
}
}

// On CD-ROM in file invoke/ex3/TabbyCat.java
class TabbyCat extends Cat{

void someMethod() {
super.someMethod();
}
}

类TabbyCat的someMethod ()方法的字节码如下:
0 aload_0 // Push obj ref from local var 0
// invoke Cat‘s someMethod() on obj ref
1 invokespecial #4 <Method void someMethod() of class Cat>
4 return // return void from TabbyCat‘s someMethod()

如果这里使用invokevirtual指令,那么将会调用类TabbyCat的somcMethod ()方法。但因为这里使用了invokespecial指令,并且常量池入口(这里是#4 )指明调用的是类Cat中声明的 someMethod ()方法,因此Java虚拟机将会准确无误地调用超类(类Cat)的someMethod()方法。

Java虚拟机是否使用静态绑定来执行invokespecial指令(或者使用一种特殊的动态绑定)取决于所指向的类是否设定了ACC_SUPER标志。JDK 1.0.2版本以前的各个版本中,invokespecial指令的名称为invokenonvirtual,而且总会导致静态绑定的使用,结果却是invokenonvirtual所坚持的静态绑定无法保证所有情况下Java语言语义的正确实现(换句话说,这是指令集中的一个 “Bug")。在JDK 1,0.2版本中,invokenonvirtual指令更名为invokespecial,它的语义也改变了。 此外,java class文件中的access_flags项中还加人了一个新的标志:ACC_SUPER。

class文件的ACC_SUPER标志项指明,Java虚拟机应该使用哪一种语义来执行在class文件字节码中所遇到的invokespecial指令。如果没有设置ACC_SUPER标志,虚拟机将会使用旧语义 (invokenonvirtual);如果设置ACC_SUPER标志,虚拟机将会使用新语义。所有新版本的Java编译器都会在生成的dass文件中默认设置ACC_SUPER标志。

根据旧语义,当执行invokespecial指令时,虚拟机将会在任何情况下都使用静态绑定。而新语义除了调用超类方法之外,其他情况下一律使用静态绑定。

根据新语义,当Java虚拟机解析一个invokespecial指令中指向超类方法的符号引用时,它会动态搜寻当前类的超类,找到离得最近的超类中的该方法的实现(换句话说,就是在继承树中与当前类最接近的超类中所声明的方法的实现)。在大多数情况下,虚拟机很可能发现最近方法实现存在于符号引用中列出的超类中。但是虚拟机也可能在另外一个不同的超类中发现最近的方法实现。

例如,如果创建了一棵包含3个类的继承树(这三个类是Animal、Dog和CockerSpaniel )。假设类Dog是Animal类的子类,CockerSpaniel类是Dog类的子类,在CockerSpanid类中定义了一 个方法,它使用invokespecial来调用一个名为walk ()的非私有的超类方法。再假设当编译CockerSpaniel时,编译器设定了ACC_SUPER标志。此外,假设当编译CockerSpaniel类时, Animal类定义了walk ()方法,但Dog类没有定义该方法。此时,CockerSpaniel类中指向walk () 方法的符号引用将会把Animal类作为它的类。当执行CockerSpaniel类中的invokespecial指令时, 虚拟机会进行动态选择,并调用Animal类的walk ()方法。

如果后来在Dog类中加人了walk()方法,并且重新编译Dog类,但是没有重新编译 CockerSpaniel类,它的指向超类的walk ()方法的符号引用仍然会将Animal作为自己的类,尽管在Dog类中已经有了walk()方法的实现。不过,当执行CockerSpaniel类中的invokespecial指令时,虚拟机将会动态选择并调用Dog类中的walk()方法的实现。

19.4 指令invokeinterface

操作码invokeinterface与invokevirtual的功能相同:它调用实例方法并使用动态绑定。这两条指令的区别在于:当引用的类型为类的时候,使用invokevirtual;当引用类型为接口时,使用 invokeinterface。

Java虚拟机使用不同于类引用的操作码来调用接口引用的方法,这是因为Java不能像使用类引用那样,使用许多与方法表偏移量相关的假设(方法表在第8章中提及)。对于类引用来说, 无论对象实际的类是什么,方法在方法表中始终占据相同的位置。但对于接口引用来说,情况就不是这样了。位于不同类中的同一方法所占据的位置是不同的,尽管这些类实现同一个接口。 19.5节列出了在字节码中使用invokeinterface的例子。

19.5指令的调用和速度

可想而知,调用接口引用方法可能要比调用类引用方法慢。因为,当Java虚拟机遇到 invokevirtual指令时,它把实例方法的符号引用解析为直接引用,所生成的直接引用很可能是方法表中的一个偏移量,而且从此往后都可以使用同样的偏移量。但对于invokeinterface指令来说,虚拟机每一次遇到invokeinterface指令,都不得不重新搜寻一遍方法表,因为虚拟机不能够假设这一次的偏移量与上一次的偏移量相同。

最快的指令是invokespecial和invokestatic,因为这些指令调用的都是静态方法。当Java虚拟机为这些指令解析符号引用时,将符号引用转换成为直接引用,所生成的直接引用将包含一个指向实际操作码的指针。

19.6方法调用的实例

下面的代码举例说明了Java虚拟机调用方法的多种途径,并对各种情况下适合使用什么样的 操作码做出了演示。
// On CD-ROM in file invoke/ex4/InYourFace.java
interface InYourFace {
void interfaceMethod ();
}

// On CD-ROM in file invoke/ex4/ItsABirdItsAPlaneItsSuperclass.java
class ItsABirdItsAPlaneItsSuperclass implements InYourFace {

ItsABirdItsAPlaneItsSuperclass(int i) {
super(); // invokespecial (of an <init>)
}

static void classMethod() {
}

void instanceMethod() {
}

final void finalInstanceMethod() {
}

public void interfaceMethod() {
}
}

19.7从方法中返回
下列几种操作码具有使方法返回的功能——每一种操作码对应一种返回的数据类型,如表19-3所示,这些操作码都没有操作数。如果有返回值,它必须被放置在操作数栈中。返回值从操作数栈中弹出,然后又被压入调用方法栈帧的操作数栈中。弹出当前栈帧,调用方法的栈帧成为当前栈帧。程序计数器被重置,其值为调用方法中紧随调用返回方法那条指令的下一条指令。 指令ireturn用于返回int、char、byte或者short数据类型的方法。
表19-3抛出异常

 

以上是关于第19章 方法的调用与返回的主要内容,如果未能解决你的问题,请参考以下文章

RTX——第19章 SVC 中断方式调用用户函数(后期补历程)

第19章 SVC中断方式调用用户函数

第1章 知识回顾

第2.6章:方法的定义与使用

第7章 类的高级特性

第3章 方法的重载及参数传递