前两篇写到可变长参数、foreach循环、自动装箱/拆箱和条件编译,这篇讨论下java的泛型与类型擦除。
泛型与类型擦除
泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
泛型思想早在C++语言的模板(Templates)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。
例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,那Object转型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会被转嫁到程序运行期之中。
泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,List<int>与List<String>
就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型。
Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码。
先看个例子
public static void main(String[] args){
List <Integer> listInt=new ArrayList <Integer>();
List <String> listString=new ArrayList <String>();
Map<String, String> map = new HashMap<String, String>();
map.put("AAA", "BBB");
map.put("CCC", "DDD");
System.out.print(map.get("AAA"));
}
编译出来的代码
public static void main(String[] paramArrayOfString)
{
ArrayList localArrayList1 = new ArrayList();
ArrayList localArrayList2 = new ArrayList();
HashMap localHashMap = new HashMap();
localHashMap.put("AAA", "BBB");
localHashMap.put("CCC", "DDD");
System.out.print((String)localHashMap.get("AAA"));
}
因此对于运行期的Java语言来说,ArrayList <Integer>与ArrayList<String>
编译出来的代码是一样的,所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。
泛型与重载
首先看看重载(Overloading)的定义:
Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义。调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法, 这就是多态性。
重载的时候,方法名要一样,但是参数类型和个数不一样,返回值类型可以相同也可以不相同。
无法以返回型别作为重载函数的区分标准。
上面也说了,泛型编译出来的代码是会把类型擦除的,所以如下的代码是不能编译的,是因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两个方法的特征签名变得一模一样,或者说两个一模一样的方法不能共存在一个class文件里。
public void method(List<String> list) {
System.out.println(list.get(0));
}
public void method(List<Integer> list) {
System.out.println(list.get(0));
}
那么如果加上返回类型呢?
public String method(List<String> list) {
System.out.println(list.get(0));
return "";
}
public int method(List<Integer> list) {
System.out.println(list.get(0));
return 1;
}
在eclipse 上仍然报错,参考重载定义的第三点无法以返回型别作为重载函数的区分标准
一点拓展
上面加上了返回类型的两个方法,在eclipse上编译不通过,但在Javac编译器中是可以编译的,编译出来的代码如下:
public String method(List<String> paramList)
{
System.out.println((String)paramList.get(0));
return "";
}
public int method(List<Integer> paramList) {
System.out.println(paramList.get(0));
return 1;
}
那是不是说明重载可以以返回类型作区分呢?不是的。因为像以下这样的代码用javac也编译不了
`public String method(String list) {
System.out.println(list);
return "";
}
public int method(String list) {
System.out.println(list);
return 1;
} `
网上找到一段引用:
在《Java虚拟机规范第二版》(JDK 1.5修改后的版本)的“§4.4.4 Signatures”章节及《Java语言规范第三版》的“§8.4.2 Method Signature”章节中分别都定义了字节码层面的方法特征签名,以及Java代码层面的方法特征签名,特征签名最重要的任务就是作为方法独一无二不可重复的ID,在Java代码中的方法特征签名只包括了方法名称、参数顺序及参数类型,而在字节码中的特征签名还包括方法返回值及受查异常表。
根据上面的例子说明:由于List<String>和List<Integer>
擦除后是同一个类型,只能添加两个并不需要实际使用到的返回值才能完成重载。这是否是一种引入泛型后的折中的解决方案呢?
最后,通过反射依然能获取到参数化的类型,说明擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息。
测试如图: