Java中short, byte, boolean和char的扩展时机

扩展概述

众所周知的是,在Java中,指令的操作码是由一个字节组成的,这意味着操作码的取值范围在0-255之间。由此带来了一个问题,对于部分和类型相关的指令——比如load——来说,并不能做到给每一个类型都设计一个对应的指令。而在Java虚拟机中,针对short, byte, boolean和char都是使用int类型的指令来完成的。举个例子来说,从局部变量表里面加载一个short类型的数据到操作数栈,将会使用iload指令。
Java虚拟机对此有明确的说明。但是这又会有另外一个问题:short, byte, boolean和char都是“短”类型,它们的字节数都要比int类型的少。所以在使用int类型的指令的时候,就需要解决扩展的问题。扩展的问题可以分成两点:

  • 如何扩展
  • 什么时候扩展

第一个问题,如何扩展,Java虚拟机规范指出short和byte两种类型将执行符号扩展,而boolean和char类型将执行零扩展。
而第二个问题,什么时候扩展,Java虚拟机规范只有一句含糊的“可以在编译期或者运行期”。这似乎意味着虚拟机的实现可以自己决定什么时候扩展。但这有点一厢情愿,因为在虚拟机规范中的指令说明中,其实就已经限定了扩展的时机。
本文将主要探讨一下在Oracle JDK 1.7.0_80版本下,使用Hotspot虚拟机的情况下,Java中short, byte, boolean和char的扩展时机。

直觉猜想

一种直觉上的认知是,扩展会带来性能的损耗,因此能够在编译期扩展完成,就没有必要在运行期扩展。基于这样的想法,可以提出一个猜想:所有的字面量,都会在编译期完成扩展。而除了字面量以外,其余的变量、参数等,因为在编译期无法确定其值,因此无法在编译期完成扩展。
早前我在思考这个问题的时候陷入了一个误区:所有的boolean和char类型的值(包括变量等)都能够在编译期完成扩展。这是基于这两个类型使用无符号扩展所产生的。这两个类型执行无符号扩展意味着,不论它们的确切值是什么,总可以将高位置为0而完成扩展。与之对应的是,因为short和byte执行符号扩展,在编译期无法确定其符号的情况下,则不能实现扩展。
这个想法之所以错误是因为没有考虑到程序被编译成字节码之后的本质。对于字节码来说,不存在一个操作数,其高位是确定的(执行扩展得来的),而低位是不确定的。所有的操作数,在编译期都是确定无误的。
因此问题最终就归结为:是否所有的字面量,都是在编译期完成扩展?

分析

是否所有的字面量,都在编译期完成了扩展?
很显然,答案是否定的。
这里采用char作为例子来说明:

public class CharExtend {
    private static final char a=0x1234;//4660
    private static char b=0x1357;//4951
    private final char c=0x2468;//9320
    private char d=0x9876;//39030

    public char testChar(char g, final char h){
        char e=0x9753;//38739
        final char f=0x8642;//34370
        b=a+c;
        d=a+c;
        d+=a;
        d+=f;
        b+=d;
        b+=h;
        b+=e;
        g+=b;
        return g;
    }

    public static char testChar1(char i, final char j, final char k){
        char l=0x4567;//17767
        final char m=0xabcd;//43981
        b+=a;
        b+=i;
        b+=j;
        b+=k;
        b+=l;
        b+=m;
        return b;
    }
}

因为不确定final,static关键字是否会对扩展造成影响,所以需要尽可能的覆盖这些关键字的所有的组合。
要想确定它们的扩展时间,还需要阅读它们生成的字节码文件。这并不是指阅读javap命令所生成的内容,而是指,直接读二进制内容。因对javap命令会屏蔽掉这个扩展的信息。举个例子来说,假如有一个字面量1,那么javap解析出来的只会是1,但是看不出来它是0x00000001还是0x0001。不过因为二进制文件读起来十分困难,可以使用javap解析的内容进行辅助。
这个类编译生成的二进制内容和javap解析得到的内容此处就不贴了。下面对a进行分析: a的十六进制是0x1234,在二进制中出现了三次:

0300 0012 3401 0001 6201 0001 6303 0000
b500 042a 59b4 0004 1112 3460 92b5 0004
0836 04b2 0007 1112 3460 92b3 0007 b200
  • 第一次是出现在常量池,对应的二进制是0x00001234,其前面的03是常量池中Integer类型的表示。可以看到的是,它已经被扩展了;
  • 第二次和第三次都是出现在0x111234这样一个串中,而11是sipush指令。在这两次出现中a并没有被扩展;

对其余变量、参数的分析这里不一一说明。总体上可以观察得到:

  • 出现在常量池中的都已经被扩展了;
  • 出现在类文件的Code属性中,使用bipush, sipush指令的,都没有被扩展;
  • 一个字面量可能同时出现在常量池和Code中;
  • static的修饰对扩展时机没有影响;
  • final修饰的成员变量的字面量,必然会出现在常量池中,而作为局部变量或者参数的修饰,则不具有这种效果;

所以问题可以进一步归结为:什么因素会影响编译器决定将字面量放入常量池,或者放入Code属性?这个问题,Java虚拟机规范没有太多的信息。唯一和此有关的就是:对于short, byte, boolean, char以及小的int类型的值来说,可以使用bipush, sipush或者iconst指令。这三种指令都明确指出,扩展是在指令执行的时候同时进行的。而大的值就会出现在常量池中。

结论

第一,是否在编译期扩展和值的大小有关,值如果偏大,那么编译器会在常量池中存有该字面量。而在常量池中字面量,必然是被扩展了的;
第二,有final关键字修饰的成员变量,其值必然会被扩展,并且放入常量池;
第三,对于一个偏小的值,但是又属于final修饰的成员变量,那么它会存有一份扩展了的常量池副本,也会在Code属性中保持未扩展的形式;
第四,其余情况都只能在运行时扩展;

你可能感兴趣的:(Java中short, byte, boolean和char的扩展时机)