JVM—编译器之解语法糖

JVM—编译器之解语法糖

众所周知JAVA的编译器包含两种:

  • 前端编译器(编译器前端):指的是把.java源文件编译成.class字节码文件,而我们平时大部分人所挂在嘴边或被知道也就是指的前端编译器

  • 运行时编译器(JIT:Just in Time Compile):指的是把.class字节码转换成机器码的过程,编译器的绝大部分优化是在这个时候做的,原因是JVM是一个平台,在运行时做优化可以针对所有在JVM上运行的编程语言(如果这个时候你还认为JAVA语言理所应当可以运行在JVM上的话,你可能没有理解JVM是一个平台的意义)

而本文,笔者将主要赘述跟我们编码息息相关的前端编译器,而本文后续所指的编译器如无例外说明,全部指的是前端编译器。因为从java编译到class字节码是一个繁琐且复杂的过程,所以本文中笔者不会讲述解析与填充符号表语法树注解处理器处理过程等复杂且枯燥的内容,如文中有涉及会提前介绍。

语法糖

语法糖:也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

JAVA属于一种“低糖语法”的语言(相对C#及其他JVM语言),使用JAVA语言编程的过程中语法糖随处可见,如:泛型自动包装自动拆箱内部类变长参数等,在虚拟机运行时并不支持这些语法,所以在编译器会把这些语法编译成JVM运行时能读懂的class字节码,这个过程叫:解语法糖,接下来我们来分析语法糖在JAVA中的使用

自动包装/拆箱

我们先来看下比较简单的包装和拆箱,请读者仔细分析下面程序,得出结果:

public class SugarTest {

public static void main(String[] args) {
    Integer a = 1;
    Integer b = 2;
    Integer c = 3;
    Integer d = 3;
    Integer e = 128;
    Integer f = 128;
    Long h = 3L;

    System.out.println(c == d);             //问题1 想知道答案?
    System.out.println(e == f);             //问题2   不告诉你!
    System.out.println(c == (a + b));       //问题3   就不告诉你!
    System.out.println(e.equals(f));        //问题4   自己想啊...
    System.out.println(h == (a + b));       //问题5   好吧,我下面会说的...
   }
}

正确的结果是:true 、false 、true 、true 、true

我们都知道Java会自动包装和拆箱,接下来我们去看看Java是怎么实现自动包装和拆箱的,前面已经介绍过自动包装和拆箱其实是一种Java的语法糖,编译器会在编译成class的时候解语法糖,那么我们反编译出java的class文件就知道编译器最后把语法糖解成什么了,下面是反编译后的代码

public class SugarTest {
    public SugarTest() {//编译器为我们加的默认构造函数
    }

    public static void main(String[] args) {
        Integer a = Integer.valueOf(1); 
        Integer b = Integer.valueOf(2);
        Integer c = Integer.valueOf(3);
        Integer d = Integer.valueOf(3);
        Integer e = Integer.valueOf(128);
        Integer f = Integer.valueOf(128);
        Long h = Long.valueOf(3L);
        System.out.println(c == d);
        System.out.println(e == f);
        System.out.println(c.intValue() == a.intValue() + b.intValue());
        System.out.println(e.equals(f));
        System.out.println(h.longValue() == (long)(a.intValue() + b.intValue()));
    }
}

很容易看出来,当我们Integer a = 1的时候实际上编译器会帮我们编译成Integer a = Integer.valueOf(1);而当Integer遇到算数运算的时候编译器会帮我们编译成调用initValue()拆箱去计算,除此之外还有什么时候会自动拆箱?当然Integer的equals()不属于编译器帮我们自动拆箱的,而是方法里面使用的是.intValue()去比较的,这里就不说了,有兴趣朋友可以自己去看源码。

到这里为止,我相信很多读者最大的疑问是在于问题1和问题2的结果。 我们再来看为什么 c==d 为true e==f为fasle?关于这个问题 我们先来看看Integer的valueOf()代码

public static Integer valueOf(int i) {
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

事实上当在 IntegerCache.low =< i <= IntegerCache.high区间的时候返回的是IntegerCache.cache[i + (-IntegerCache.low)] cache是一个Integer数组,而超出这个范围valueOf将return一个new出来的新Integer,所以用 e==f 返回的是false,而这个IntegerCache.low和IntegerCache.high默认分别是 -128和127,也就是说当Integer的值在-128和127之间的时候编译器或者说Java会自动包装和拆箱 ,否则用 == 比较的时候返回的是false,所以这个地方大家在编码的时候就需要注意了!而这个low和high的范围我们可以通过 -XX:AutoBoxCacheMax=设置,下面我们看下JDK源码对IntegerCache的注释

/**
 * Cache to support the object identity semantics of autoboxing for values between
 * -128 and 127 (inclusive) as required by JLS.
 *
 * The cache is initialized on first usage.  The size of the cache
 * may be controlled by the -XX:AutoBoxCacheMax=<size> option.
 * During VM initialization, java.lang.Integer.IntegerCache.high property
 * may be set and saved in the private system properties in the
 * sun.misc.VM class.
 */

泛型语法糖

看完了自动拆箱/包装,我们再来看下Java泛型,有意思的是Java泛型和C#不一样的是,C#的泛型是真的底层实现,对于C#来说List<String>和List<User>是两种不同的类型,而在Java他们被认为是一种类型,看下面的例子:

public void updateUser(List<String> ids){}

public void updateUser(List<User> users){}

上述代码在Java中会编译失败,提示已经有相同的方法存在,这是因为Java编译器会把泛型在编译成class的时候解语法糖,去掉泛型的类型(也就是你听说过的泛型擦除),所以List<'String>和List<'User>是两种相同的类型,然后我们再来看下反编译后的泛型代码:

public static void updateUser(List<SugarTest.User> users) {}

这个时候奇怪的是class反编译过来的代码却保存着泛型User,而想想平时开发中我们也可以用反射获取泛型的类型,这跟上述的泛型擦除是否相矛盾?事实上获取泛型的类型在有时候是必须的,所以编译器在class文件中留有一个字节保存着泛型的原始类型,也就是User。

如今,我们已经不知道Java为什么使用语法糖去完善泛型语法,或许是因为Java有历史遗留问题,毕竟泛型是1.5后推出的。但这种形式的泛型在我们开发中还是会带来一些不便,最简单的就是上述的例子。

Switch支持String类型

JDK1.7推出了Switch语法支持String类型,这给使用Switch开发带来了便利,那我们就来看下Java是怎么实现的呢?

//源码
switch ("xxxx") {
        case "xxxx": {
            System.out.println("xxx");
            break;
        }
    }

//反编译后的代码   
String var8 = "xxxx";
byte var9 = -1;
switch(var8.hashCode()) {
case 3694080:
    if(var8.equals("xxxx")) {
        var9 = 0;
    }
default:
    switch(var9) {
    case 0:
        System.out.println("xxx");
    default:
    }
}

有了前面的分析方法,我们可以很直观的看出来编译器把String的Switch语句编译成使用String的HashCode()去做的,也就是说事实上运行时虚拟机仍然只支持switch(int)

变长数组

变长数组也是java语法糖的实现之一,我们常常会看到这种下法:

public void printSome(String... str){}

printSome("1","2","3");

看看编译器帮我们编译成String数组

printSome(new String[]{"1", "2", "3"});

总结

以上就是我们看到在Java中的语法糖,除了文中列举的语法,还包括内部类枚举等一系列语法,尤其是Integer、Long(和Integer一样)等数值的自动包装和拆箱在用的过程中一定要注意。


转载请注明出处:http://my.oschina.net/ambitor/blog/542789

转载于:https://my.oschina.net/ij5IYLKW/blog/542789

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值