为什么引入泛型
泛型,即“参数化类型”。是Java 1.5引入的一种新特性。
为什么Java要引入泛型呢?我们看一下这个例子:
1 | List arrayList = new ArrayList(); |
毫无疑问,程序的运行结果会以崩溃结束:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
这个例子中,我们构造了一个ArrayList列表,并往其中放入了一个String类型与一个Integer类型。而在使用时,我们都是以String类型的方式来强转取出,因此程序崩溃了。
为了让这种类似的容器类能够带上其内容的类型信息,解决类似的类型转换问题,泛型应运而生。
有了泛型之后,我们的例子可以优化成这个样子:
1 | List<String> arrayList = new ArrayList<String>(); |
泛型能让编译器在编译阶段避免不少类似的类型转换问题。
泛型的使用
泛型的使用主要有三种:
- 泛型类:通常定义于各种容器类中,如List<T>
- 泛型方法:提供类型参数推断的功能,很方便
- 泛型构造方法:博主感觉与泛型方法类似,只是为了区分普通方法与构造方法,因此有了泛型方法与泛型构造方法
从Java的Type体系中我们可以看出三种泛型的定义:
Type是Java中所有类型的公共高级接口,其子类如下:
- ParameterizedType:参数化类型,即泛型;例如:List<T>、Map<K,V>等带有参数化的类
- TypeVariable:类型变量,即泛型中的变量;例如:T、K、V等变量,可以表示任何类;在这需要强调的是,TypeVariable代表着泛型中的变量,而ParameterizedType则代表整个泛型
- GenericArrayType:泛型数组类型,用来描述ParameterizedType、TypeVariable类型的数组;即List<T>[] 、T[]等
- Class:上三者不同,Class是Type的一个实现类,属于原始类型,是Java反射的基础,对Java类的抽象
- WildcardType:泛型表达式(或者通配符表达式),即? extend Number、? super Integer这样的表达式
其中,TypeVariable的接口定义如下:
1 | public interface TypeVariable<D extends GenericDeclaration> extends Type { |
可以看到,它也是一个泛型类,它的泛型表示的是:它所表示的类型变量的具体种类,是泛型类、泛型方法还是泛型构造方法
GenericDeclaration的子类如下图所示:
从图中可以看出:Method、Constructor、Class分别表示类、方法以及构造方法三种泛型
更多泛型的使用与特性可以参考博主以前的文章:
泛型常用特点,List<String>能否转为List<Object>
泛型的擦除
泛型的擦除可谓是Java泛型的一大特点,那么什么是泛型擦除呢?
博主认为:泛型擦除即我们无法获得某个泛型实例对象的精确泛型参数
我们可以从两个现象来看这个特点:
- 泛型类中的编译器静态检查
- 泛型类反射
泛型类中的编译器静态检查
我们来看下下面的例子:
1 | class A { |
当我们的编译器执行静态检查时,是没有运行时的信息的
因此,对于例子中的泛型类C,编译器只能把它的泛型参数T当做其上界来处理(即把类型信息擦除到边界),由于Object类为所有类的父类,因此编译器检查时T都是被当成Object类来处理的
当然这种情况我们可以使用extends关键字来改善,对于例子中的泛型类D,其泛型参数的上界就变为了类A
泛型类反射
要解释这个,我们需要先知道Java提供的反射功能是什么
Java的反射功能,是Java提供的一种允许我们在运行时,获取某个类(class)全部信息的能力,包括类的成员变量,方法,构造函数等等,同时还提供了一系列设置与调用的手段
我们来看下下面的例子:
1 | class A {} |
D中的f方法通过获取D的Class类来获取其类型信息,其打印的结果如下:
可以看到打印出来的结果并不是我们传人的泛型参数A、B、C三个类,这是为什么呢?
博主认为,这是因为这里用的是Java的反射机制获取D类的类型信息,而泛型参数实际上应该属于生成实例时传入的参数,因此我们只能获取类相关的信息,即TypeVariable(类型变量)以及它的名称T,而不能获取实例相关的泛型实际参数A、B、C
受这个特点影响比较大的要数如Gson等支持反序列化泛型对象的工具了:
1 | class Foo<T> { |
上面是一段Gson的官方例子,可以想象到从foo实例中通过反射获取Foo类中的泛型参数是失败的
由于泛型参数的缘故,我们只能拿到TypeVariable(类型变量)以及它的名称T,并不能获得实际的Bar类
官方介绍链接:
https://github.com/google/gson/blob/master/UserGuide.md#serializing-and-deserializing-generic-types
那么有没有什么方法可以缓解这种问题呢?
当然是有的,反射可以帮助我们获取类型相关的信息,因此这种情况我们只需要继承实现一个子类就可以了:
1 | Class Foo<T> { |
在继承实现子类的时候,由于我们显式地定义了SubFoo类的基类Foo的泛型参数,因此通过Java的反射,我们也能轻松的获取相关的泛型信息
可以看到Gson官方提供的解决方案也是类似的:
1 | Type fooType = new TypeToken<Foo<Bar>>() {}.getType(); |
通过继承创建匿名内部类TypeToken后,再使用Java的反射机制,获取相关的泛型信息(TypeToken内部的源码在此就不进行分析了)
补充
泛型其实是Java中的一种语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家Peter.J.Landin发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用
Java中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。虚拟机并不支持这些语法,它们在编译阶段就被还原回了简单的基础语法结构,这个过程成为解语法糖
Java语言在JDK1.5之后引入的泛型实际上只在程序源码中存在,在编译后的字节码文件中,就已经被替换为了原来的原生类型,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList和ArrayList就是同一个类。所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型