您的位置:首页 > 其它

JVM—编译器之解语法糖

2015-12-11 00:00 190 查看
摘要: JAVA属于一种“低糖语法”的语言(相对C#及其他JVM语言),使用JAVA语言编程的过程中语法糖随处可见,如:泛型、自动包装、自动拆箱、内部类、变长参数等,在虚拟机运行时并不支持这些语法,所以在编译器会把这些语法编译成JVM运行时能读懂的class字节码...

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: