一直以来对Java泛型都处于一知半解的状态,趁着最近细读Java编程思想读到泛型章节,做个笔记备忘。
一、伪泛型
Java的主要涉及灵感来自于C++,很多地方都有相似之处。但是在泛型(C++里面的模板)的实现方式上却有较大的差异。导致差异的根本原因在于Java5之前Java不支持泛型,而要做到前后兼容必须做出妥协,找出一个折中的方式——type erasure(类型擦除)。类型擦除的意思是参数化类型只存在于编译期,编译通过后,在之后生成的Java字节码中是不包含泛型中的类型信息的。
比如在代码中定义的ArrayList<object>和ArrayList<String>等类型,在编译后都会变成ArrayList。JVM看到的只是ArrayList,而由泛型具体的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。
ArrayList<String> a = new ArrayList<>();
ArrayList<Integer> b = new ArrayList<>();
System.out.println(a.getClass());
System.out.println(b.getClass());
System.out.println(a.getClass() == b.getClass());
/**
output:
class java.util.ArrayList
class java.util.ArrayList
true
*/
在编译时,一旦编译器确认泛型类型是安全使用的,就会将它转换为原始类型。还是以ArrayList为例:
ArrayList<String> list= new ArrayList<String>();
list.add("泛型");
//list.add(1); 编译报错
String str = list.get(0);
可以看到编译器会发现于泛型类型不符的非法操作,在正确性验证通过后,以上代码会被翻译成如下代码:
ArrayList list= new ArrayList();
list.add("泛型");
String str = (String)(list.get(0));
list是ArrayList类的实例,而不是ArrayList<String>的。另外,注意到从list中去除第一个值加上了强制转换,强转的类型也即是我们定义的泛型类型。在引入泛型之前,这本应是我们程序猿做的事,因为在ArrayList里面是一个Object数组来存储我们放进去的对象引用,所以拿出来需要我们强转之后再进行接下来的具体操作。引入泛型之后,只要我们定义了泛型类型,编译器就会为我做这个工作,这样还能规避很多非法转型。
前面提到,Java之所以采用“伪泛型”很大一部分原因是因为JDK1.5之前压根就没有泛型概念,为了向后兼容代码库而不得不采取的一种折中办法。假如在以前的代码库中有这样一个方法:
public void func(List list){
//do sonmething
}
JDK1.5之后容器类都用泛型重新编写,你的代码里大多数都是List<Integer>之类的定义,不然编译器会给你个警告表达他的不满。如果没有泛型擦除,那么像List<Integer>将是不同于List的全新类型,由于List<Integer>和List没有直接的继承关系,所以也没法强制转型,所以你想向上面的那个方法传递你的List<Integer>就不太可能了。在1.5之前已经有太多的代码库,显然全部用泛型重新写一遍是不太现实的事,所以就把所有泛型类都在编译期统一到原始类型,这样就可以愉快地使用1.5之前的代码库了。
二、忙碌的编译器
上述的擦除、转型之类的工作其实都是编译器一个人在忙碌,但就是因为编译器做的工作有些繁杂,导致刚接触泛型的童鞋迷失在这些工作里面。这里先上Java编程思想里面的一句话,个人觉得精髓至极:
在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。记住,“边界就是发生动作的地方”。
所以,总的来说,编译器对泛型类主要有以下两个工作:
- 在泛型类和其方法内部进行擦除,将参数类型擦除到第一个边界处;
- 在泛型类的方法调用处进行传递参数的类型检查和返回值的转型;
对于第一条举几个简单的栗子:
package blog.xu;
class A{}
interface B{}
interface C{}
public class GenericType<T> {
T value;
public void set(T value){
this.value = value;
}
public T get(){
return value;
}
}
借助命令
javap -c GenericType.class
可以看到反编译后的字节码:
public class blog.xu.GenericType<T> {
T value;
public blog.xu.GenericType();
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: return
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #23 // Field value:Ljava/lang/Object;
5: return
public T get();
Code:
0: aload_0
1: getfield #23 // Field value:Ljava/lang/Object;
4: areturn
}
可以看到在set和get方法中的value类型都变成了Object,所以,在没有限定类型边界的情况下会统一擦除到Object。而泛型边界则复用了Java关键字extends,现将类的定义改为:
public class GenericType<T extends A&B&C>
可得到如下代码:
public class blog.xu.GenericType<T extends blog.xu.A & blog.xu.B & blog.xu.C> {
T value;
public blog.xu.GenericType();
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: return
public void set(T);
Code:
0: aload_0
1: aload_1
2: putfield #23 // Field value:Lblog/xu/A;
5: return
public T get();
Code:
0: aload_0
1: getfield #23 // Field value:Lblog/xu/A;
4: areturn
}
可以看到value的类型变为class A,而这里的A恰好是第一个边界。这里需要注意的地方是如果边界中存在类而不是接口,则必须将类放在第一个,否则会报错。