早起的方法类型泛化:在jdk1.5诞生之前,只能通过Object是所有类共同父类和类型强制转换着两个特点来实现类型的泛化,但是这样做有一个很严重的缺陷:只有程序员和运行时期的虚拟机才知道 要被转换的Object到底是什么类型的对象。在编译期,编译器无法检查这个Object对象能否被强制转换成功,仅仅通过程序员去保证这个操作,很多风险会被转嫁到程序的运行期。
加入版本:JDK1.5
泛型本质:对“参数化类型”的应用,也就是说把“所操作的数据的类型”被指定为一个参数。
参与对象:这种“参数化类型”可以参与到类、接口和方法的创建中,分别称之为泛型类、泛型接口和泛型方法。
在说到Java的泛型,我们应该明确一点:Java的泛型只是一个语法糖,仅起到在编译器之前提升语义准确性的作用。
下面我们将详细说明该观点的原因。
首先我们对C#和java的泛型做个对比:
c# | Java | 对比结果 | |
使用方式 | List<String> List<int> | List<String> List<Integer> | 基本相同 |
编译后 | List<String> List<int> | List List | 不同 |
由上面表格看出:
C#中的泛型,在编译后是切实存在的,List<String>和List<int>在编译后完全是两个不同的类型,他们在系统运行期间生成,都拥有自己的虚表方法和类型数据,这种泛型的实现方式称之为:类型膨胀,基于这种方法实现的泛型称为“真实泛型”。
Java中的泛型,只在源码中存在,在编译后的字节码中,会被替换为原来的原生类型(裸类型),并且会在相应的地方插入强制转换的代码。对于运行期来说,List<Integer>和List<String>是同一个类型List。这种实现泛型的方法称之为:类型擦除,基于它实现的泛型称为:伪泛型。
例如下列代码:
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("hello");
list.add("java");
System.out.println(list.get(0));
System.out.println(list.get(1));
}
将包含上面代码的类编译,再将生成的class文件反编译得到下列代码。
public static void main(String args[]){
List list = new ArrayList();
list.add("hello");
list.add("java");
System.out.println((String)list.get(0));
System.out.println((String)list.get(1));
}
可以看到,反编译之后所有泛型都消失了,取而代之是取值时的强制转换。泛型类型变成了原生类型。
于是当泛型遇到重载的时候,我们会看到这种现象:
public class Test {
public static String send(List<String> list){
return "";
}
public static String send(List<Integer> list){
return "";
}
}
“伪泛型”作为参数时并不能使方法重载,因为编译时的擦除动作会导致上面两个方法的的特征签名变得一模一样,在同一个Class文件中无法共存。(顺便回顾一下java方法重载的要求——具备不同的特征签名,包括:参数类型、参与顺序)
另一个例子:
public class Test {
public static String send(List<String> list){
return "";
}
public static int send(List<Integer> list){
return 0;
}
}
仅仅变化了方法2的返回值,这个代码又能编译了(测试时请使用JDK1.6版本的 javac编译器进行编译,其他编译器可能不会支持)。
我们知道在java中是不能通过返回值来重载方法的,上面这个例子中之所能编译成功,是因为在两个方法中加入了不同的返回值后,在编译后的class文件中这两个方法的描述符是不同的,所以能在同一个Class文件中共存,且能正常被运行。
最后一点:类型擦除中的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,所以我们可以通过反射来取得参数化类型。