aas 几乎所有的编程语言都或多或少提供过一些语法糖来方便程序员的代码开发,这些语法糖虽然不会提供实质性的功能改进,但是它们或能提高效率,或能提升语法的严谨性,或能减少编码出错的机会。现在也有一种观点认为语法糖并不一定都是有益的,大量添加和使用含糖的语法,容易让程序员产生依赖,无法看清语法糖的糖衣背后,程序代码的真实面目。
asdsadasdasdasdsadasdasdasdsadassdasdsasdsadsdasdasadasdsadassadasdas————《Java虚拟机规范》
Java语法糖
aa
泛型:
aas
aas泛型的本质是参数化类型或者参数化多态的应用,即可以将操作的数据类型指定为方法签名中的一种特殊参数,这种参数类型能够用在类、接口和方法的创建中,分别构成泛型类、泛型接口和泛型方法。泛型让程序员能够针对泛化的数据类型编写相同的算法,这极大地增强了编程语言的类型系统及抽象能力。
aas[注]:Java和C#两门语言各自添加了泛型的语法特性,但实现方式却截然不同,其实Java的泛型直到今天依然作为Java语言不如C#语言好用。
aa
aasC#实现的泛型
aa
aaasdass实现方式:“具现化式泛型”,C#里面泛型无论在程序源码里面、编译后的中间语言表示(这时候泛型是一个占位符)里面,抑或是运行期的CLR (common language runtime) 里面都是切实存在的。
aaasdsads[注]:List< int>与List< string>就是两个不同的类型,它们由系统在运行期生成,有着自己独立的虚方法表和类型数据。
aa
aasJava实现的泛型
aa
aaasdass实现方式:“类型擦除式泛型”,它只在程序源码中存在,在编译后的字节码文件中,全部泛型都被替换为原来的裸类型。并且在相应的地方插入了强制转型代码。
aaasdsads[注]:对于Java语言来说,ArrayList< int>与ArrayList< String>其实是同一个类型。
aas比较:
aa
aaasdass①、Java中不支持对泛型进行实例判断、不支持使用泛型创建对象、不支持使用泛型创建数组。
aa
aaasdass②、C#2.0引入了泛型之后,带来的显著优势之一便是对比起Java在执行性能上的提高,因为在使用平台提供的容器类型(如List< T>,Dictionary<TKey,TValue>)时,无须像Java里那样不厌其烦地拆箱和装箱,如果在Java中要避免这种损失,就必须构造一个与数据类型相关的容器类(譬如IntFloatHashMap这样的容器)。显然,这除了引入更多代码造成复杂度提高、复用性降低之外,更是丧失了泛型本身的存在价值。
aa
aaasdass③、Java中擦除式泛型的实现几乎只需要在Javac编译器上做出改进即可,不需要改动字节码、不需要改动Java虚拟机,也保证了以前没有使用泛型的库可以直接运行在Java 5.0之上。
aa
aas类型擦除:
aa
aaasdass为了保证编译出来的Class文件可以在Java 5.0引入泛型之后继续运行,有两种方式:
aaasdaasdass①、将已有的类型泛型化(Java选择)
aaasdaasdass②、平行地加一套泛型化版本的新类型(C#选择)
aaasdass要让所有需要泛型化的已有类型,譬如ArrayList,原地泛型化后变成了ArrayList< T>,而且保证以前直接用ArrayList的代码在泛型新版本里必须还能继续用这同一个容器,这就必须让所有泛型化的实例类型,譬如ArrayList< Integer>、ArrayList< String>这些全部自动成为ArrayList的子类型才能可以,否则类型转换就是不安全的。由此就引出了“裸类型”的概念,裸类型应被视为所有该类型泛型化实例的共同父类型 。 只有这样,我们才能实现如下转型:
ArrayList<Integer> ilist = new ArrayList<Integer>();
ArrayList<String> slist = new ArrayList<String>();
ArrayList list; // 裸类型
list = ilist;
list = slist;
aas如何实现裸类型呢(2种方法)?
aa
aaasdass①、在运行期由Java虚拟机来自动地、真实地构造出ArrayList< Integer>这样的类型,并且自动实现从ArrayList< Integer>派生自ArrayList的继承关系来满足裸类型的定义;
aa
aaasdass②、简单粗暴地直接在编译时把ArrayList< Integer>还原回ArrayList,只在元素访问、修改时自动插入一些强制类型转换和检查指令,这样看起来也是能满足需要,这两个选择的最终结果大家已经都知道了(Java选择的实现方式)。
aa
aasdsasdsadsadsadsadasdsadadasdas泛型擦除前的例子:
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?"));
}
aaasdass【注】:把这段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了裸类型。
aa
aasdsasdsadsadsadsadasdsadadasdas泛型擦除后的例子:
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?"));
}
aas擦除式泛型的缺陷:
aa
aasdas①、使用擦除法实现泛型直接导致了 对原始类型数据的支持 又成了新的麻烦。
aasdsadasd
aasdsasdsadsadsadsadasdsadadasdas原始类型的泛型(目前的Java不支持)
ArrayList<int> ilist = new ArrayList<int>();
ArrayList<long> llist = new ArrayList<long>();
ArrayList list;
list = ilist;
list = llist;
aasasaas这种情况下,一旦把泛型信息擦除后,到了要插入强制转型代码的地方就没办法往下做了,因为不支持int、long与Object之间的强制转型。
aasasaas解决方法:当遇到原生类型(int 、long)时,对其进行装箱、拆箱,变成ArrayList< Integer>、ArrayList< Long>。
aaasdass【注】:装箱、拆箱的开销是Java泛型慢的重要原因。也成为今天Valhalla项目要重点解决的问题之一。
aa
aasdas②、运行期无法取到泛型类型信息。会让一些代码变得相当啰嗦。比如不支持对泛型进行实例判断、不支持使用泛型创建对象、不支持使用泛型创建数组,都是由于运行期Java虚拟机无法取得泛型类型而导致的。
aaasdass【注】:比如我们去写一个泛型版本的从List到数组的转换方法,由于不能从List中取得参数化类型T,所以不得不从一个额外参数中再传入一个数组的组件类型进去。
aasdsadasd
aasdsasdsadsadsadssadsaadasdsadadasdas不得不加入的类型参数
public static <T> T[] convert(List<T> list, Class<T> componentType) {
T[] array = (T[])Array.newInstance(componentType, list.size());
...
}
aa
aasdas③、通过擦除法来实现泛型,还丧失了一些面向对象思想应有的优雅,带来了一些模棱两可的模糊状况。
aasdsadasd
aasdsasdsadsadsadssadsaadasdsadadasdas当泛型遇见重载1
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)");
}
}
aasasaas分析:这段代码是不能被编译的,因为参数List< Integer>和List< String>编译之后都被擦除了,变成了同一种的裸类型List, 类型擦除导致这两个方法的特征签名变得一模一样。初步看来,无法重载的原因已经找到了,但是真的就是如此吗?其实这个例子中泛型擦除成相同的裸类型只是无法重载的其中一部分原因。再看:
aasdsadasd
aasdsasdsadsadsadssadsaadasdsadadasdas当泛型遇见重载2
public class GenericTypes {
public static String method(List<String> list) {
System.out.println("invoke method(List<String> list)");
return "";
}
public static int method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
return 1;
}
public static void main(String[] args) {
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
}
执行结果:
invoke method(List<String> list)
invoke method(List<Integer> list)
aasasaas分析:这里的重载当然不是根据返回值来确定的,之所以这次能编译和执行成功,是因为两个method()方法加入了不同的返回值后才能共存在一个Class文件之中。 由于List< String>和List< Integer>擦除后是同一个类型,我们只能添加两个并不需要实际使用到的返回值才能完成重载,这是一种毫无优雅和美感可言的解决方案,并且存在一定语意上的混乱。
aaasdass【注】:前面的文章已经提到:方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名中,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说两个方法如果有相同的名称和特征签名,但返回值不同(JVM层面就是 特征签名不同),那它们也是可以合法地共存于一个Class文件中的。
aaasdass总结:
aaaasdsasas①、由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响并带来新的需求,如在泛型类中如何获取传入的参数化类型等。所以《Java虚拟机规范》做出了相应的修改,引入了诸如Signature、LocalVariableTypeTable等新的属性用于解决伴随泛型而来的参数类型的识别问题。
aaaasdsasasdas⒈Signature:存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。
aaaasdsasas②、从Signature属性的出现我们还可以得出结论:擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们在编码时能通过反射手段取得参数化类型的根本依据。
自动装箱、拆箱与遍历循环
aa
aaas就纯技术的角度而论,自动装箱、自动拆箱与遍历循环 这些语法糖,无论是实现复杂度上还是其中蕴含的思想上都不能与泛型相提并论,两者涉及的难度和深度都有很大差距。
aa
aaas我们看一段代码,其中包含了泛型、自动装箱、自动拆箱、遍历循环与变长参数5种语法糖:
aasdsadasd
aasdsasdsadsadsadssadsaadasdsadadasdas编译前的代码
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
int sum = 0;
for (int i : list) {
sum += i;
}
System.out.println(sum);
}
aasdsasdsadsadsadssadsaadasdsadadasdas编译后的代码
public static void main(String[] args) {
List list = Arrays.asList( new Integer[] { //泛型消除
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4) });
int sum = 0;
for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
int i = ((Integer)localIterator.next()).intValue();
sum += i;
}
System.out.println(sum);
}
aasasaas分析: ⒈泛型:编译后泛型消除,List< Integer>变成了List 。
aasasasdasdaas⒉自动装箱、拆箱:编译之后被转化成了对应的包装和还原方法,如本例中的Integer.valueOf()
与intValue()
方法,
aasasasdasdaas⒊遍历循环:编译之后代码还原成了 迭代器 的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。
aasasasdasdaas⒋变长参数:它在调用的时候变成了一个数组类型的参数,在变长参数出现之前,程序员的确也就是使用数组来完成类似功能的 。
aasdsasdsadsadsadssadsaadasdsadadasdas自动封装带来的陷阱:
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.print(c == d ); true
System.out.print(e == f ); false //超出了范围[-128,127],所以封装的地址不一样了
System.out.print(c == (a + b) ); true //自动拆箱了,
System.out.print(c.equals(a + b) ); true
System.out.print(g == (a + b) ); true //“==”运算遇到了"+",自动拆箱
System.out.print(g.equals(a + b) ); false //equals()方法不处理数据转型的关系
}
执行结果:
true false true true true false
aasasaas分析:包装类的 “==”运算 在不遇到算术运算的情况下不会自动拆箱,以及它们equals()方法不处理数据转型的关系,在实际编码中尽量避免这样使用自动装箱与拆箱。
条件编译
aa
aaasC、C++中使用预处理器指示符(#ifdef)来完成条件编译。C、C++的预处理器最初的任务是解决编译时的代码依赖关系(如极为常用的#include预处理命令)。
aa
aaasJava语言实现条件编译,方法就是使用条件为常量的if语句,该代码中的if语句不同于其他Java代码,它在编译阶段就会被“运行”。
aasdsasdsadsadsadssadsaadasdsadadasdasJava语言的条件编译:
public static void main(String[] args) {
if (true) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}
aasdsasdsadsadsadssadsasadadasdas该代码编译后Class文件的反编译结果:
public static void main(String[] args) {
System.out.println("block 1");
}
aaasJava语言中条件编译的实现,也是Java语言的一颗语法糖,根据布尔常量值的真假,编译器将会把分支中不成立的代码块消除掉,这一工作将 在编译器解除语法糖阶段(com.sun.tools.javac.comp.Lower类中)完成。
aaas由于这种条件编译的实现方式使用了if语句,所以它必须遵循最基本的Java语法,只能写在方法体内部,因此它只能实现语句基本块(Block)级别的条件编译,而没有办法实现根据条件调整整个Java类的结构。
aaas总结:除了文章种说到的泛型、自动装箱、自动拆箱、遍历循环、变长参数和条件编译之外,Java语言还有不少其他的语法糖,如内部类、枚举类、断言语句、数值字面量、对枚举和字符串的switch支持、try语句中定义和关闭资源(这3个从JDK 7开始支持)、Lambda表达式(从JDK 8开始支持,Lambda不能算是单纯的语法糖,但在前端编译器中做了大量的转换工作),等等。