JVM—编译器之解语法糖
2015-12-11 00:00
190 查看
摘要: JAVA属于一种“低糖语法”的语言(相对C#及其他JVM语言),使用JAVA语言编程的过程中语法糖随处可见,如:泛型、自动包装、自动拆箱、内部类、变长参数等,在虚拟机运行时并不支持这些语法,所以在编译器会把这些语法编译成JVM运行时能读懂的class字节码...
前端编译器(编译器前端):指的是把
运行时编译器(JIT:Just in Time Compile):指的是把
而本文,笔者将主要赘述跟我们编码息息相关的
JAVA属于一种“低糖语法”的语言(
正确的结果是:true 、false 、true 、true 、true
我们都知道Java会自动包装和拆箱,接下来我们去看看Java是怎么实现自动包装和拆箱的,前面已经介绍过自动包装和拆箱其实是一种Java的语法糖,编译器会在编译成class的时候解语法糖,那么我们反编译出java的class文件就知道编译器最后把语法糖解成什么了,下面是反编译后的代码
很容易看出来,当我们
到这里为止,我相信很多读者最大的疑问是在于问题1和问题2的结果。 我们再来看为什么 c==d 为true e==f为fasle?关于这个问题 我们先来看看Integer的
事实上当在
上述代码在Java中会编译失败,提示已经有相同的方法存在,这是因为Java编译器会把泛型在编译成class的时候解语法糖,去掉泛型的类型(也就是你听说过的泛型擦除),所以List<'String>和List<'User>是两种相同的类型,然后我们再来看下反编译后的泛型代码:
这个时候奇怪的是class反编译过来的代码却保存着泛型User,而想想平时开发中我们也可以用反射获取泛型的类型,这跟上述的泛型擦除是否相矛盾?事实上获取泛型的类型在有时候是必须的,所以编译器在class文件中留有一个字节保存着泛型的原始类型,也就是User。
如今,我们已经不知道Java为什么使用语法糖去完善泛型语法,或许是因为Java有历史遗留问题,毕竟泛型是1.5后推出的。但这种形式的泛型在我们开发中还是会带来一些不便,最简单的就是上述的例子。
有了前面的分析方法,我们可以很直观的看出来编译器把String的Switch语句编译成使用String的HashCode()去做的,也就是说事实上运行时虚拟机仍然只支持switch(int)
看看编译器帮我们编译成String数组
转载请注明出处:http://my.oschina.net/ambitor/blog/542789
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
相关文章推荐
- Eclipse 提交至Git@OSC
- Eclipse 3.7如何安装egit
- html中input标签reset的使用
- CentOS卸载自带OpenJdk
- snmp agent
- vc chart control加载
- AFN数据解析简介
- NSURLSession详解
- NSCache深入浅出
- 位移枚举解析
- Microsoft.Aspnet.SingR2实例
- thread_ para
- linux 进程间通信 pipe
- linux 进程间通信 fifo
- linux 进程间通信 fifo_read
- linux 进程间通信fifo_write
- panic in gccgo
- discuz门户发布文章编辑名字用realname
- php开发工具 zend studio 12.5.1 中文版汉化
- 【嘉兴东臣php】HTML+CSS+JS基础学习周总结