1.前言
本文记录内容来自《深入理解Java虚拟机》的第十章早期(编译期)优化其中一节内容,其他的内容个人觉得暂时不需要过多关注,比如语法、词法分析,语义分析和字节码生成的过程等。主要关注的就是Java的一些语法糖是如何实现的。
语法糖不会提供实质性的功能改进,但是它们或能提高效率,或能提升语法的严谨性,或能减少编码出错的可能。大量使用语法糖可能会迷失其中,不得要领,下面就介绍一下Java的语法糖的实现。
2.泛型与类型擦除
JDK5新增了一个特性,就是泛型。本质是参数化类型的应用,就是所操作的数据类型被指定为一个参数,这种参数可以应用在类、接口和方法的创建中。
Java没有泛型的时候,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化,比如HashMap的get方法,返回的就是Object,因为map中一切皆有可能。但是这样的操作带来了一些风险,如果强转的类型错误,就会在运行期间抛出异常,我们需要在编码期间就发现这个问题。
Java的泛型是一种伪泛型,不是C#那种传统意义上的泛型。在C#中,List<int>和List<String>是两种类型,但是Java中还是List。因为Java的泛型只存在于代码中,编译后泛型就消失了,这个就是所说的类型擦除,取而代之的就是插入了强转代码。将一段含有泛型的Java程序编译后,再反编译回来,就会发现反编译的代码中泛型消失了,新增的就是强转代码。
为什么要使用类型擦除的方式实现泛型的原因不得而知,但是这个确实是个吐槽的地方。有人说性能上泛型会由于强制转型操作和运行期缺少针对类型的优化导致速度比真正的泛型慢。这个评价角度不太对,因为泛型是用于提升语义准确性的。Java的泛型会导致一些奇特的问题。
比如重载:public static void method(List<String> list)和public static void method(List<Integer> list)这两个方法存在是不会编译通过的,因为类型擦除后是特征签名一模一样的。看似重载不正确的原因找到了:重载要求方法名相同,参数不同,返回值可同可不同。这里参数、方法名一致肯定失败了。但实际上不是,因为如果改一下一个返回String类型,一个返回Integer类型,按照重载的定义,返回值可以不同,这两个方法应该还是一样的,编译不同过才对,而实际上是可以通过的。这又是为什么呢?在Java语言中方法重载实际是要求方法具有不同的特征签名,相同的方法名。特征签名是一个方法中各个参数在常量池中的字段符号引用的集合,返回值不在其中,这样也就能理解重载的要求是方法名相同,参数不同的含义了。而修改了返回值为什么通过了呢?原因在于这不是重载的范畴,在Class文件格式中,只要描述符不是完全一致的两个方法也可以共存。方法的描述符和返回值是有关系的,所以这两个方法的描述符因为返回值的区别是不同的,可以共存,但不是重载。
3.自动装箱、拆箱与遍历循环
从技术角度来说,这些语法糖在实现和思想上都不如泛型,但是这是使用最多的语法糖。
自动装箱的操作就是:Integer.valueof(i),在编译阶段替换成了这个
自动拆箱的操作就是:Integer.intValue()
集合的foreach操作是:for(its = list.iterator; its.hasNext; ;) { its.next},采取的是迭代器的方式遍历
数组的foreach操作实际上是压栈,进栈出栈操作。
这些原理都什么简单,可以自己写一个类编译后,再反编译看看结果。
自动装箱的陷阱:
public static void main(String[] args) {
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.println(e == f);
System.out.println(c == (a+b));
System.out.println(c.equals(a+b));
System.out.println(g == (a+b));
System.out.println(g.equals(a+b));
}
上面的代码执行出来1.true 2.false 3.true 4.true 5.true 6.false。
解释一下上面的含义:
首先包装类的==比较不会触发自动拆箱,所以1、2的比较是引用比较。那为什么1是true,2是false?上面说了自动装箱是通过Integer.valueof实现的,Java代码对Integer进行了优化,查看源代码可以看到有个缓存,在-128~127之间的整数,获取的是同一个Integer对象,321超过了这个范围,生成的是不同的对象。
+号会触发自动拆箱,导致==一般是int类型,而不是Integer类型,最后触发c的自动拆箱,两个int类型比较,值相等就是true了。
4中的+号也会触发自动拆箱,成int类型,但由于是equals方法,又进行了自动装箱,最后判断标准就是Integer.equals的方法,其比较就是两个int类型进行比较,所以是true
5中和3的步骤类似,但是g自动才行成了long类型,long与int比较,数值的大小比较。
6和4类似,但是最终变成了Long.equals(Integer),根据Long的equals方法,传入的必须是Long类型才可,所以返回的是false。
这个例子告诉我们代码中避免这样使用自动装箱和拆箱,掌握不牢可能发生意料之外的情况。
4.条件编译
许多语言都提供了条件编译的途径,比如C、C++的预处理器指示符#ifdef来完成条件编译,他们是用于解决编译时代码的依赖关系,但在Java中没有使用预处理器,因为不需要,编译器不是一个个编译Java文件,而是将所有编译单元的语法树顶级节点输入到待处理列表后再进行编译。
Java语言也可以实现条件编译,方法就是使用条件是常量的if语句,比如if(true){} else{},这样else模块的内容就会被省略。
这种条件编译只能写在方法体内,没有办法根据条件调整整个Java类结构。
5.其他语法糖
内部类、枚举类、断言语句、对枚举和字符串(JDK7)的switch支持、try中定义和关闭资源等。这些可以自己通过javac编译后javap -verbose进行查看字节码,理解其实现原理。