JVM(十五:语法糖)
final
// 方法一带有final修饰
public void foo(final int arg) {
final int var = 0;
// do something
}
// 方法二没有final修饰
public void foo(int arg) {
int var = 0;
// do something
}
观察这两段代码编译出来的字节码,会发现它们是没有任何一点区别的,每条指令,甚至每个字节都一模一样。可以肯定地推断出把局部变量声明为final,对运行期是完全没有影响的,变量的不变性仅仅由Javac编译器在编译期间来保障,这就是一个只能在编译期而不能在运行期中检查的例子。
语法糖
指的是在计算机语言中添加的某种语法,这种语法对语言的编译结果和功能并没有实际影响,但是却能更方便程序员使用该语言。通常来说使用语法糖能够减少代码量、增加程序的可读性,从而减少程序代码出错的机会。
Java中最常见的语法糖包括了前面提到过的泛型、变长参数、自动装箱拆箱,等等,Java虚拟机运行时并不直接支持这些语法,它们在编译阶段被还原回原始的基础语法结构,这个过程就称为解语法糖。
泛型
在2004年,Java和C#两门语言于同一年更新了一个重要的大版本,即Java 5.0和C#2.0,在这个大版本中,两门语言又不约而同地各自添加了泛型的语法特性。本来Java和C#天生就存在着比较和竞争,泛型这个两门语言在同一年、同一个功能上做出的不同选择,自然免不了被大家对比审视一番,其结论是Java的泛型直到今天依然作为Java语言不如C#语言好用的“铁证”被众人嘲讽。
Java选择的泛型实现方式叫作“类型擦除式泛型”(Type Erasure Generics),而C#选择的泛型实现方式是“具现化式泛型”(Reified Generics)。
Java的泛型确实在实际使用中会有一些限制,如果读者是一名C#开发人员,可能很难想象以下Java代码都是不合法的。
public class TypeErasureGenerics<E> {
public void doSomething(Object item) {
if (item instanceof E) { // 不合法,无法对泛型进行实例判断
...
}
E newItem = new E(); // 不合法,无法使用泛型创建对象
E[] itemArray = new E[10]; // 不合法,无法使用泛型创建数组
}
}
上面这些是Java泛型在编码阶段产生的不良影响,如果说这种使用层次上的差别还可以通过多写几行代码、方法中多加一两个类型参数来解决的话,性能上的差距则是难以用编码弥补的。C#2.0引入了泛型之后,带来的显著优势之一便是对比起Java在执行性能上的提高,因为在使用平台提供的容器类型(如List<T>,Dictionary<TKey,TValue>)时,无须像Java里那样不厌其烦地拆箱和装箱,如果在Java中要避免这种损失,就必须构造一个与数据类型相关的容器类(譬如IntFloatHashMap这样的容器)。显然,这除了引入更多代码造成复杂度提高、复用性降低之外,更是丧失了泛型本身的存在价值。
Java的类型擦除式泛型无论在使用效果上还是运行效率上,几乎是全面落后于C#的具现化式泛型,而它的唯一优势是在于实现这种泛型的影响范围上:擦除式泛型的实现几乎只需要在Javac编译器上做出改进即可,不需要改动字节码、不需要改动Java虚拟机,也保证了以前没有使用泛型的库可以直接运行在Java 5.0之上。
这是由于《Java语言规范》中的对Java使用者的严肃承诺,譬如一个在JDK 1.2中编译出来的Class文件,必须保证能够在JDK 12乃至以后的版本中也能够正常运行。这样,既然Java到1.4.2版之前都没有支持过泛型,而到Java 5.0突然要支持泛型了,还要让以前编译的程序在新版本的虚拟机还能正常运行,就意味着以前没有的限制不能突然间冒出来。
设计者面前大体上有两条路可以选择:
1)需要泛型化的类型(主要是容器类型),以前有的就保持不变,然后平行地加一套泛型化版本
的新类型。
2)直接把已有的类型泛型化,即让所有需要泛型化的已有类型都原地泛型化,不添加任何平行于已有类型的泛型版。
在这个分叉路口,C#走了第一条路,添加了一组System.Collections.Generic的新容器,以前的System.Collections以及System.Collections.Specialized容器类型继续存在。C#的开发人员很快就接受了新的容器,倒也没出现过什么不适应的问题,唯一的不适大概是许多.NET自身的标准库已经把老容器类型当作方法的返回值或者参数使用,这些方法至今还保持着原来的老样子。
但如果相同的选择出现在Java中就很可能不会是相同的结果了,要知道当时.NET才问世两年,而Java已经有快十年的历史了,再加上各自流行程度的不同,两者遗留代码的规模根本不在同一个数量级上。而且更大的问题是Java并不是没有做过第一条路那样的技术决策,在JDK 1.2时,遗留代码规模尚小,Java就引入过新的集合类,并且保留了旧集合类不动。这导致了直到现在标准类库中还有Vector(老)和ArrayList(新)、有Hashtable(老)和HashMap(新)等两套容器代码并存,如果当时再摆弄出像Vector(老)、ArrayList(新)、Vector<T>(老但有泛型)、ArrayList<T>(新且有泛型)这样的容器集合,可能叫骂声会比今天听到的更响更大。
泛型擦除前的例子
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("hello", "你好");
map.put("how are you?", "吃了没?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you?"));
}
把这段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了裸类型,只在元素访问时插入了从Object到String的强制转型代码
泛型擦除后的例子
public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "你好");
map.put("how are you?", "吃了没?");
System.out.println((String) map.get("hello"));
System.out.println((String) map.get("how are you?"));
}
问题:
1,使用擦除法实现泛型直接导致了对原始类型(Primitive Types)数据的支持又成了新的麻烦。因为不支持int、long与Object之间的强制转型。当时Java给出的解决方案一如既往的简单粗暴:既然没法转换那就索性别支持原生类型的泛型了吧,你们都用ArrayList<Integer>、ArrayList<Long>,反正都做了自动的强制类型转换,遇到原生类型时把装箱、拆箱也自动做了得了。这个决定后面导致了无数构造包装类和装箱、拆箱的开销,成为Java泛型慢的重要原因,也成为今天Valhalla项目要重点解决的问题之一。
2,运行期无法取到泛型类型信息,我们去写一个泛型版本的从List到数组的转换方法,由于不能从List中取得参数化类型T,所以不得不从一个额外参数中再传入一个数组的组件类型进去,实属无奈。
public static <T> T[] convert(List<T> list, Class<T> componentType) {
T[] array = (T[])Array.newInstance(componentType, list.size());
...
}
3,擦除法来实现泛型,还丧失了一些面向对象思想应有的优雅,带来了一些模棱两可的模糊状况
public class GenericTypes {
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
}
这段代码是不能被编译的,因为参数List<Integer>和List<String>编译之后都被擦除了,变成了同一种的裸类型List,类型擦除导致这两个方法的特征签名变得一模一样。
为了弥补擦除的不足,新增了Signature属性。Signature属性的作用是存储一个方法在字节码层面的特征签名,这个属性保存的参数类型不是原生类型,而是包括了参数化类型的信息。
泛型与类型擦除;Java泛型-4(类型擦除后如何获取泛型参数)
摘抄:《深入理解Java虚拟机:JVM高级特性与最佳实践》-第十章