早期(编译器)优化--Java语法糖的味道

时间:2022-10-29 19:26:30

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>()):
} }

执行结果


因为两个返回值的加入,方法重载居然成功了。这是对java语言中返回值不参与重载选择的基本认知的挑战吗?

之所以能够编译和执行成功,是因为两个method()方法中加入了不同的返回值后才能够共存在一个Class文件之中。Class文件方法表中提到过,方法重载要求方法具备不同的特征签名,返回值并不包括在方法的特征签名里,所以返回值不参与重载选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存,也就是说,两个方法如果相同的名称和特征签名,但返回值不同,那他们也是可以合法地共存于一个Cass文件中的!

!!!擦拭法所谓的擦除,仅仅是对方法的code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能够通过反射手段取得参数化类型的根本依据。

既然所有的泛型都会被擦拭,为什么不能往List<String>里面加int类型的数据呢?在Myeclipse里面这样会直接报错,说明是在编译之前就进行的,我想不是应该发生在语义分析里面的标注检查那个阶段么??解除语法糖发生在语义分析之后,说明之前会发生标注检查,应该就是这样吧。

②自动装箱、拆箱和遍历循环

java语言里面使用最多的语法糖。

public static void main(String[] args){

 List<Integer> list=Arrays.asList(,,,);
int sum=;
for(int i:list){
sum+=i; } }

解除语法糖之后

public static void main(String[] args){

 List list=Arrays.asList(new Integer[]{
Integet.valueOf();
Integet.valueOf();
Integet.valueOf();
Integet.valueOf();
}
int sum=;
for(Iterator l=list.iterator();l.hasNext();){ int i=((Ingeter)l.next()).intValue();
sum+=;
} }

变长参数调用的时候变成了一个数组类型的参数,Integer.valueOf()与Integer.intValue()为包装和还原方法。

自动装箱的错误用法

  public static void main(String[] args){
Integer a=;
Integer b=;
Integer c=;
Integer d=;
Integer e=;
Integer f=;
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包装类中定义了一个私有的静态内部类如下:

早期(编译器)优化--Java语法糖的味道
 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 }
早期(编译器)优化--Java语法糖的味道

而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"): }