早期(编译器)优化--Java语法糖的味道
1.泛型与类型擦除
泛型的本质是参数化类型的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。在泛型没有出现之前,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化,由于java语言所有的类型都继承自Object,因此Object转型成任何对象都是有可能的,但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转换是否成功,只能寄托于程序员不会出错,许多ClassCatException的风险就会转嫁到程序运行期之中
C#的泛型在程序源码在、编译后的IL中,或是运行期的CLR中,都是切实存在的,List<int>和List<String>就是两种不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型
java语言中的泛型不一样,只在源码中存在,在编译后的字节码文件中,已经替换成了原来的原生类型(也称为裸类型)了,并且在相应的地方插入了强制转化代码,因此,对于运行期的Java语言来说,ArrayList<int>和ArrayList<String>就是同一个类,所以泛型技术实际上是java语言的一颗语法糖,java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。
public static void main(String[] args){ Map<String,String> map=new HashMap<String,String>(); map.put("hello","nihao"); map.put("how are you","chifan"); System.out.println(map.get("hello")); System.out.println(map.get("how are you")); }
这段代码编译成Class文件,然后用字节码反编译工具进行反编译后,泛型类型都变回了原生类型。
public static void main(String[] args){ Map map=new HashMap(); map.put("hello","nihao"); map.put("how are you","chifan"); System.out.println((String)map.get("hello")); System.out.println((String)map.get("how are you")); }
通过擦除法来实现泛型丧失了一些泛型思想应有的优雅。
public class G{ public static void method(List<String> list){ //输出1 } public static void method(List<Integer> list){ //输出2 } }
很明显不能编译通过,因为擦除以后两种方法的方法签名变得一模一样。好像不能重载的原因找到了?只能说泛型擦除成相同的原生类型这时无法重载的一部分原因,继续看
public class G{ public static String method(List<String> list){ //输出1
return ""; } public static int method(List<Integer> list){ //输出2 return 1; } public static void main(String[] args){ method(new ArrayList<String>()): method(new ArrayList<Integer>()): } }
执行结果
1 2
因为两个返回值的加入,方法重载居然成功了。这是对java语言中返回值不参与重载选择的基本认知的挑战吗?
之所以能够编译和执行成功,是因为两个method()方法中加入了不同的返回值后才能够共存在一个Class文件之中。Class文件方法表中提到过,方法重载要求方法具备不同的特征签名,返回值并不包括在方法的特征签名里,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存,也就是说,两个方法如果相同的名称和特征签名,但返回值不同,那他们也是可以合法地共存于一个Cass文件中的!
!!!擦拭法所谓的擦除,仅仅是对方法的code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能够通过反射手段取得参数化类型的根本依据。
既然所有的泛型都会被擦拭,为什么不能往List<String>里面加int类型的数据呢?在Myeclipse里面这样会直接报错,说明是在编译之前就进行的,我想不是应该发生在语义分析里面的标注检查那个阶段么??解除语法糖发生在语义分析之后,说明之前会发生标注检查,应该就是这样吧。
②自动装箱、拆箱和遍历循环
java语言里面使用最多的语法糖。
public static void main(String[] args){ List<Integer> list=Arrays.asList(1,2,3,4); int sum=0; for(int i:list){ sum+=i; } }
解除语法糖之后
public static void main(String[] args){ List list=Arrays.asList(new Integer[]{ Integet.valueOf(1); Integet.valueOf(2); Integet.valueOf(3); Integet.valueOf(4); } int sum=0; for(Iterator l=list.iterator();l.hasNext();){ int i=((Ingeter)l.next()).intValue(); sum+=1; } }
变长参数调用的时候变成了一个数组类型的参数,Integer.valueOf()与Integer.intValue()为包装和还原方法。
自动装箱的错误用法
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)); }
true false true true true false
包装类的“==”运算在不遇到算术运算的情况下不会自动装箱,以及它们的equals()方法不处理数据类型转型的关系。
1. 首先我们明确一下"=="和equals方法的作用。
"==":如果是基本数据类型,则直接对值进行比较,如果是引用数据类型,则是对他们的地址进行比较(但是只能比较相同类型的对象,或者比较父类对象和子类对象。类型不同的两个对象不能使用==)
equals方法继承自Object类,在具体实现时可以覆盖父类中的实现。看一下Object中qeuals的源码发现,它的实现也是对对象的地址进行比较,此时它和"=="的作用相同。而JDK类中有一些类覆盖了Object类的equals()方法,比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:
java.io.file,java.util.Date,java.lang.string,包装类(Integer,Double等)。
2. Java的包装类实现细节。观察源码会发现Integer包装类中定义了一个私有的静态内部类如下:
1 private static class IntegerCache { 2 static final int low = -128; 3 static final int high; 4 static final Integer cache[]; 5 6 static { 7 // high value may be configured by property 8 int h = 127; 9 String integerCacheHighPropValue = 10 sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); 11 if (integerCacheHighPropValue != null) { 12 try { 13 int i = parseInt(integerCacheHighPropValue); 14 i = Math.max(i, 127); 15 // Maximum array size is Integer.MAX_VALUE 16 h = Math.min(i, Integer.MAX_VALUE - (-low) -1); 17 } catch( NumberFormatException nfe) { 18 // If the property cannot be parsed into an int, ignore it. 19 } 20 } 21 high = h; 22 23 cache = new Integer[(high - low) + 1]; 24 int j = low; 25 for(int k = 0; k < cache.length; k++) 26 cache[k] = new Integer(j++); 27 28 // range [-128, 127] must be interned (JLS7 5.1.7) 29 assert IntegerCache.high >= 127; 30 } 31 32 private IntegerCache() {} 33 }
而Integer的自动装箱代码:
1 public static Integer valueOf(int i) { 2 if (i >= IntegerCache.low && i <= IntegerCache.high) 3 return IntegerCache.cache[i + (-IntegerCache.low)]; 4 return new Integer(i); 5 }
通过观察上面的代码我们可以发现,Integer使用一个内部静态类中的一个静态数组保存了-128-127范围内的数据,静态数组在类加载以后是存在方法区的,并不是什么常量池。在自动装箱的时候,首先判断要装箱的数字的范围,如果在-128-127的范围则直接返回缓存中已有的对象,否则new一个新的对象。其他的包装类也有类似的实现方式,可以通过源码观察一下。
3. "=="在遇到非算术运算符的情况下不会自动拆箱,以及他们的equals方法不处理数据类型转换的关系。
因此,对于 System.out.println(c == d); 他们指向同一个对象,返回True。
对于 System.out.println(e == f); 他们的值大于127,即使值相同,但是对应不同的内存地址,返回false。
对于 System.out.println(c == (a+b)); 自动拆箱后他们的值是相等的,返回True。
对于 System.out.println(c.equals(a+b)); 他们的值相同,而且类型相同,返回true。
对于 System.out.println(g == (a+b)); 自动拆箱后他们的值相等,返回True。
对于 System.out.println(g.equals(a+b)); 他们的值相同但是类型不同,返回false。
③条件编译
(—般情况下,C语言源程序中的每一行代码.都要参加编译。但有时候出于对程序代码优化的考虑.希望只对其中一部分内容进行编译.此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译)
C、C++使用预处理器指示符(#ifdef)来完成条件编译。而在java预言之中没有使用预处理器,因为java语言天然的编译方式(编译器并非一个个地编译java文件,而是将所有编译单元的语法树顶级节点输入到待处理列表后再进行编译,因此各个文件之间能够互相提供符号信息)无须使用预处理器。
java想实现条件编译,方法就是使用条件为常量的if语句。这时if语句不通气其他java代码,它在编译阶段就会被“运行”,生成字节码之中只包括if里面的内容,不包括它分支else里面的内容
public static void main(String[] args){ if(true){ System.out.println("bolck 1"); }else{ System.out.println("bolck 2"); } }
编译后会变成
public static void main(String[] args){ System.out.println("bolck 1"): }