Java语法糖的味道

                                                                                   -----摘自周志明 《深入理解Java虚拟机》

泛型

泛型的本质是参数化类型(Parameterized Type)或者参数化多态(Parametric Polymorphism)的应用,即可以将操作的数据类型指定为方法签名中的一种特殊参数,这种参数类型能够用在类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法。泛型让程序员能够针对泛化的数据类型编写相同的算法,这极大地增强了编程语言的类型系统及抽象能力。

Java选择的泛型实现方式叫作“类型擦除式泛型”(Type Erasure Generics),而C#选择的泛型实现方式是“具现化式泛型”(Reified Generics)。#里面泛型无论在程序源码里面、编译后的中间语言表示(Intermediate Language,这时候泛型是一个占位符)里面,抑或是运行期的CLR里面都是切实存在的,List与List就是两个不同的类型,它们由系统在运行期生成,有着自己独立的虚方法表和类型数据。而Java语言中的泛型则不同,它只在程序源码中存在,在编译后的字节码文件中,全部泛型都被替换为原来的裸类型(Raw Type)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList与ArrayList其实是同一个类型,由此读者可以想象“类型擦除”这个名字的含义和来源,这也是为什么笔者会把Java泛型安排在语法糖里介绍的原因。

Java的类型擦除式泛型无论在使用效果上还是运行效率上,几乎是全面落后于C#的具现化式泛型,而它的唯一优势是在于实现这种泛型的影响范围上:擦除式泛型的实现几乎只需要在Javac编译器上做出改进即可,不需要改动字节码、不需要改动Java虚拟机,也保证了以前没有使用泛型的库可以直接运行在Java 5.0之上。

裸类型应被视为所有该类型泛型化实例的共同父类型(Super Type),只有这样,像代码清单10-4中的赋值才是被系统允许的从子类到父类的安全转型。

Java语法糖的味道_第1张图片

Java裸类型的实现方式:简单粗暴地直接在编译时把ArrayList还原回ArrayList,只在元素访问、修改时自动插入一些强制类型转换和检查指令,这样看起来也是能满足需要。

Java语法糖的味道_第2张图片

Java泛型的缺陷:

  1. 不支持像 int、long等这些原始类型(primivte types)数据,因为不支持int、long与Object之间的强制转型。当时Java给出的解决方案一如既往的简单粗暴:既然没法转换那就索性别支持原生类型的泛型了吧,你们都ArrayList、ArrayList,反正都做了自动的强制类型转换,遇到原生类型时把装箱、拆箱也自动做了得了。这个决定后面导致了无数构造包装类和装箱、拆箱的开销,成为Java泛型慢的重要原因,也成为今天Valhalla项目要重点解决的问题之一。

  1. 运行期无法取到泛型类型信息,如以下代码所示,我们去写一个泛型版本的从List到数组的转换方法,由于不能从List中取得参数化类型T,所以不得不从一个额外参数中再传入一个数组的组件类型进去,实属无奈。

Java语法糖的味道_第3张图片

  1. 丧失了一些面向对象思想应有的优雅,带来了一些模棱两可的模糊状况.第6章介绍Class文件方法表(method_info)的数据结构时曾经提到过,方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名中,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存也就是说两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个Class文件中的。

Java语法糖的味道_第4张图片
Java语法糖的味道_第5张图片
Java语法糖的味道_第6张图片

自动装箱、拆箱与遍历循环

Java语法糖的味道_第7张图片

代码清单10-11中一共包含了泛型、自动装箱、自动拆箱、遍历循环与变长参数5种语法糖,代码清单10-12则展示了它们在编译前后发生的变化。泛型就不必说了,自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法,如本例中的Integer.valueOf()与Integer.intValue()方法,而遍历循环则是把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。最后再看看变长参数,它在调用的时候变成了一个数组类型的参数,在变长参数出现之前,程序员的确也就是使用数组来完成类似功能的。

Java语法糖的味道_第8张图片

鉴于包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,以及它们equals()方法不处理数据转型的关系,笔者建议在实际编码中尽量避免这样使用自动装箱与拆箱。

出现上述结果的原因如下:

  1. 当int值在一个字节范围内(-128~127 代码中的low和high)时,Integer会有缓存(IntegerCacge),每次直接从缓存中获取, 但是超出这个范围的Integer每次都会重新new.

  1. 当int与Integer(Long)比较时,java会将Integer(Long)类型自动拆箱转换为 int(long) 再进行比较,所以到最后是 int(long) 和 int 之间的值的比较,所以无论int值是否在一个字节范围内,比较结果均为true。(long与int比较时, int会被提升为long类型)

Java语法糖的味道_第9张图片
  1. equals会先判断类型是否相同,如果类型不同则直接返回false,其次才会去判断value是否相同。

Java语法糖的味道_第10张图片

条件编译

Java语法糖的味道_第11张图片

Java语言中条件编译的实现,也是Java语言的一颗语法糖,根据布尔常量值的真假,编译器将会把分支中不成立的代码块消除掉,这一工作将在编译器解除语法糖阶段(com.sun.tools.javac.comp.Lower类中)完成

除了本节中介绍的泛型、自动装箱、自动拆箱、遍历循环、变长参数和条件编译之外,Java语言还有不少其他的语法糖,如内部类、枚举类、断言语句、数值字面量、对枚举和字符串的switch支持、try语句中定义和关闭资源(这3个从JDK 7开始支持)、Lambda表达式(从JDK 8开始支持,Lambda不能算是单纯的语法糖,但在前端编译器中做了大量的转换工作),等等,读者可以通过跟踪Javac源码、反编译Class文件等方式了解它们的本质实现,囿于篇幅,笔者就不再一一介绍了。

你可能感兴趣的:(深入理解Java虚拟机,jvm)