一直以来对Java泛型都处于一知半解的状态,趁着最近细读Java编程思想读到泛型章节,做个笔记备忘。
一、伪泛型
Java的主要涉及灵感来自于C++,很多地方都有相似之处。但是在泛型(C++里面的模板)的实现方式上却有较大的差异。导致差异的根本原因在于Java5之前Java不支持泛型,而要做到前后兼容必须做出妥协,找出一个折中的方式——type erasure(类型擦除)。类型擦除的意思是参数化类型只存在于编译期,编译通过后,在之后生成的Java字节码中是不包含泛型中的类型信息的。
比如在代码中定义的ArrayList
ArrayList a = new ArrayList<>();
ArrayList 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
前面提到,Java之所以采用“伪泛型”很大一部分原因是因为JDK1.5之前压根就没有泛型概念,为了向后兼容代码库而不得不采取的一种折中办法。假如在以前的代码库中有这样一个方法:
public void func(List list){
//do sonmething
}
JDK1.5之后容器类都用泛型重新编写,你的代码里大多数都是List
二、忙碌的编译器
上述的擦除、转型之类的工作其实都是编译器一个人在忙碌,但就是因为编译器做的工作有些繁杂,导致刚接触泛型的童鞋迷失在这些工作里面。这里先上Java编程思想里面的一句话,个人觉得精髓至极:
在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转型。记住,“边界就是发生动作的地方”。
所以,总的来说,编译器对泛型类主要有以下两个工作:
对于第一条举几个简单的栗子:
package blog.xu;
class A{}
interface B{}
interface C{}
public class GenericType {
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 value;
public blog.xu.GenericType();
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."":()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."":()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恰好是第一个边界。这里需要注意的地方是如果边界中存在类而不是接口,则必须将类放在第一个,否则会报错。