16.1条件分支
在Java源代码中,可以在一个方法中使用if、if-else、while、 do-while、for和switch语句来指定基本的控制流。当把所有源代码转换成为字节码的时候,除了switch语句外,Java编译器使用同样的操作码集。例如,Java提供的最简单的控制流是if语句。当编译一个Java程序时,针对if语句表达式的不同行为,if语句能够被转换成任意一组操作码。每一种操作码都会从栈顶端弹出一个或者两个值,然后进行比较。从栈中弹出一个值的操作码把该值与0进行比较;从栈中弹出两个值的操作码对这两个值进行比较。如果比较成功(成功在不同的操作码中定义不同), Java虚拟机将按照由比较操作码的操作数所提供的偏移量执行分支或者跳转操作。
对于所有的条件分支操作码,Java虚拟机都通过同样的过程来决定下一条将要执行的指令。 虚拟机首先执行由操作码所决定的比较。如果比较失败,虚拟机将继续执行条件分支语句后面的代码;如果比较成功,虚拟机将会使用紧随操作码后的两个操作数字节来产生一个带符号的16位的偏移量。虚拟机给当前PC寄存器加上这个偏移量(条件分支操作码的地址)来获取目标地址。目标地址必须指向同一个方法中的一条指令的操作码。程序会继续从目标地址开始运行。
如表16-1所示的一组if操作码执行对0的整数比较操作。当java虚拟机遇到上述这些操作码时, 虚拟机将会从栈中掸出一个int类型值,并将其于0进行比较。
如表16-3所示的第三组操作码对其他基本类型(long、float、 double )进行比较操作。这些操作码本身并不会执行分支操作,而是把代表比较结果的int类型值(0表示相等,1表示大于, -1表示小于)压人栈,然后使用一种前面已经介绍过的对int类型进行比较的操作码进行实际分支跳转。
用于比较float类型的两个操作码(fcmpg和fcmpl),其不同之处在于处理NaN的方式。在Java虚拟机中,如果进行比较的值之—是NaN,浮点值比较通常会失败。当两个值都不是NaN时, 如果两个值相等,fcmpg和fcmpl指令都会将0压人栈,如果第一个值大于第二个值,那么都会将1压人栈,如果第二个值大于第-个值,那么都会将-1压人栈。但当至少有-个值为NaN时, fcmpg指令会将1压人栈,而fcmpl值领会将-1压入找。由于这两个操作码都可以使用,在两个浮点数之间进行的任何比较操作都会得到相同的结果,其结果也将被压人栈,这与是否因为NaN的出现而导致失败没有关系。对于比较两个double类型值的操作码—dcmpg和dcmpl,上述结论依然成立。
如表16-4所示的第4组操作码从栈顶端弹出对象引用,将其与null进行比较。如果比较成功, Java虚拟机将会执行分支操作。
如表16-5所示的最后一组操作码从栈中弹出两个对象引用,对它们进行比较。在这种情况下, 只存在两种比较结果:“相等”或者“不相等”。如果引用相等,说明它们指向堆中同一对象; 如果不相等,说明它们指向两个不同对象。与其他所有的if操作码一样,如果比较成功,Java虚拟机执行分支操作。
16.2无条件分支
上一节描述了所有使Java虚拟机执行条件分支操作的操作码。下面描述另一组使java虚拟机进行无条件分支操作的操作码。如表16-6所示的这些操作码被称为goto指令。为了执行goto指令, 虚拟机首先根据两个紧随goto指令的操作数字节,得出一个带符号的16位偏移量(为了执行 goto_w指令,虚拟机需要首先根据紧随goto_w指令的4个操作数字节得出一个带符号的32位偏移 童),接着,虚拟机再把所得到的偏移量加到当前PC寄存器上。最后得到的地址必须指向当前方法中一条指令的操作码的位置。虚拟机将会在这条指令处继续执行。
如表16-6所示的操作码足以表述字节码中任何控制流,这些控制流在java源文件中if、if-else、while, do-while或者for语句表示。前述的操作码也能够用来表述switch语句,但Java虚拟机的指令集为switch语句专门设计了两个操作码:tableswitch和lookupswitch。
16.3使用表的条件分支
如表16-7所示,tableswitch和lookupswitch指令都包含一个默认的分支偏移量和一组可变长度的“case值/分支偏移量”对。这两条指令都会将键值(紧随switch关键字后的括号中表达式的值)从栈中弹出。它们会把键值与所有case值进行比较。如果发现匹配项,则取与该case值相关的程序分支偏移量,若未发现匹配项,则取默认程序分支偏移量。
指令tableswitch与lookupswitch之间的不同之处在于,它们采用不同的方法指定case值。指令lookupswitch比tableswitch适用的范围更广,但tableswitch具有更高的效率。这两条指令后面都有0到3个填充宇节,这是为了使紧随在填充字节后面的字节能以4字节的整数倍(从方法的开头算起)位置处开始(这两条指令是整个Java虚拟机指令集中仅有的考虑了边界对齐的多字节指令)。对于这两条指令来说,填充字节后面的4个字节容纳了默认的分支偏移量。
操作码lookupswitch之后是0到3个填充字节和4个字节的默认分支偏移量,接着就是一个4字节长的值(nairs),它指明了指令后附带的“case值/分支偏移量”对的数量。case值是一个int类型值,这充分说明了Java语言中的switch语句需要的是一个类型为int、short, char或者byte的键值表达式。如果将long、float或者double类型的值作为switch语句的键值,那么程序就无法通过编译。与每一个case值相关的程序分支偏移量都是一个4字节的偏移量。“值/分支偏移量”对必须按照case值递增的顺序依次出现。
在tableswitch指令中,紧随在0到3个填充字节和4个字节的默认分支偏移量后面的是低、高 int类型值。低、高值指明了包含在本tableswitch指令中case值的范围。在低、高值后面的是程序分支偏移量列表。列表中项数为(髙值-低值+ 1 ),列表内容为:高值程序分支偏移量、低值程序分支偏移量以及介于高值和低值之间每一个整数case值的程序分支偏移量。低值程序分支偏移量紧随高值程序分支偏移量之后。
因此,当Java虚拟机遇到一条lookupswitch指令时,它必须把键值与每个case值相比较,直到发生下列情况之一时才结束查找:发现匹配的值;检索到case值大于键值(“case值/分支偏移量“对按照case值递增的顺序排列);所有case值均检索完毕。如果虚拟机没有检索到匹配的值,它将使用默认的程序分支偏移量。而当Java虚拟机遇到tableswitch指令时,它会简单地检查键值是否位于高值和低值之间。如果不在此范围内,那么就使用默认的程序分支偏移量;如果在此范围内,虚拟机将用键值减去低值,得出一个偏移量,该偏移量列在了分支偏移量列表中。通过这样的方法,虚拟机能够确定适当的程序分支,而无需对每一个case值进行检查。
除了前面的表中所叙述的操作码之外,Java虚拟机中能够影响控制流的只有处理异常抛出与捕获、finally子句以及调用方法和从方法中返回的指令。这些操作码将在后续章节中讨论。
由javac为argue()方法产生的字节码如下所示:
3 tableswitch 0 to 1: default=2
0:24 //case 0 (TONAYTO): goto 24
1:29 //case 1(TOMANTO):goto 29
//note that the next instruction starts
//at address 24,which means the
// tableswitch took up 21 bytes
方法argue ()的功能只不过是将say的值在TOMAYTO和TOMAHTO之间来回切换。由于 TOMAYTO和TOMAHTO的值是连续的(TOMAYTO的值为0, TOMAHTO的值为1),javac编译器使用tableswitch指令。指令tableswitch比指令lookupswitch效率更高,而同等性质的 lookupswhch指令的长度为28字节-比tableswitch长4个字节。
由此可知,当TOMAYTO的值为0,而TOMAHTO的值为2时,javac编译器仍将使用 tableswitch。因为,尽管这里需要为默认值1额外分配程序分支偏移量,但tableswitch的指令的长度仍然只有28字节与功能相同的lookupswitch指令的长度相同,而tableswitch比lookupswitch效率更高,所以这里使用tableswitch。但当TOMAHTO的值为3时,javac开始使用lookupswitch指令, 因为此时tableswitch的列表中将需要两个额外的分支偏移量(1和2),这样,该指令长度就将达到32字节。指令lookupswitch的长度就会小于tableswitch,所以javac选择lookupswitch指令。