Java 5中泛型的引入成为Java程序设计语言自最初发行以来最显著的变化。泛型程序设计(generic programming)意味着编写的代码可以对多种不同类型的对象重用。在Java中增加泛型类之前,泛型程序设计是用继承实现的。
ArrayList类有一个类型参数(type parameter)用来指示元素的类型。如后所示: var a=new ArrayList<**String**>(); 如果用一个明确的类型而不是var声明一个变量,则可以通过使用”菱形”语法省略构造器中的类型参数。省略的类型可以从变量的类型推断得出。Java 9扩展了菱形语法的使用范围,原先不接受这种语法的地方现在也可以使用了。如,可以对匿名子类使用菱形语法。类型参数的魅力所在:会让程序更容易读懂,也更安全。
泛型类(generic class)就是有一个或多个类型变量的类。类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型。常见的做法是类型变量使用大写字母,而且很简短。Java库使用变量E表示集合的元素类型,K和V分别表示表的键和值的类型。T(必要时可以用相邻的字母U和S)表示“任意类型”。可以用具体的类型替换类型变量来实例化泛型类型。换句话说,泛型类相当于普通的工厂。从表面上看,Java的泛型类型类似于C++的模板类。唯一明显的不同是Java没有特殊的template关键字。
可以定义一个带有类型参数的方法。这个方法是在普通类中定义的,而不是在泛型类中。注意,类型变量放在修饰符的后面,并在返回类型的前面。当然,泛型方法可以在普通类中定义,也可以在泛型类中定义。当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面。但在C++,要将类型参数放在方法名后面。这有可能会导致解析的二义性。
有时,类和方法需要对类型变量加以约束。而在C++中,不能对模板参数的类型加以限制。Java中使用关键字extends,如后所示:<T extends BoundingTyep>。其中T应该是限定类型(bounding type)的子类型(subtype)。T和限定类型可以是类,也可以是接口。一个类型变量或通配符可以有多个限定,限定类型用 “&”分隔,而逗号用来分隔类型变量。在Java的继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定。
虚拟机没有泛型类型对象——所有对象都是属于普通类。无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限定类型(或者,对于无限定的变量则替换为Object)。原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为Object。Java的泛型与C++模板有很大的区别。C++会为每个模板的实例化产生不同的类型,这一现象称为“模板代码膨胀”。Java不存在这个问题。
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。类型擦除也会出现在泛型方法中。总之,对于Java泛型的转换,需要记住以下几点:
1)虚拟机中没有泛型,只有普通的类和方法。
2)所有的类型参数都会替换为它们的限定类型。
3)会合成桥方法来保持多态。
4)为保持类型安全性,必要时会插入强制类型转换。
设计Java泛型时,主要目标是允许泛型代码和遗留代码之间能够互操作。
不能用基本类型代替类型参数。虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。为提醒这一风险,如果试图查询一个对象是否属于某个泛型类型,编译器会报错(使用instanceof时),或给出一个警告(使用强制类型转换时)。不能实例化参数化类型的数组。需要说明的是,只是不允许创建这些数组,而声明变量仍是合法的。不过,不能使用new来初始化这个变量。
可以使用@SafeVarargs注解来消除创建泛型数组的有关限制,不过这会隐藏着危险。@SafeVarargs只能用于声明为static、final或(Java 9)private的构造器和方法。
不能在类似 new T(……)的表达式中使用类型变量。就像不能实例化泛型实例一样,也不能实例化数组。不能在静态字段或方法中引用类型变量。既不能抛出也不能捕获泛型类的对象。实际上,泛型类扩展Throwable甚至都是不合法的。catch子句不能使用类型变量。不过,在异常规范中使用类型变量是允许。Java异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器。不过可以利用泛型取消这个机制。当泛型类型被擦除后,不允许创建引发冲突的条件。泛型规范说明还引用了另外一个原则:“为了支持擦除转换,要施加一个限制:倘若两个接口类型是同一个接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类。” 总是可以将参数化类型转换为一个原始类型。最后,泛型类可以扩展和实现其他的泛型类。
在通配符类型中,允许类型参数发生变化。通配符限定与类型变量限定十分类似,但是,有一个附加能力,即可以指定一个超类型限定(supertype bound)。带有超类型限定的通配符允许写入一个泛型对象,而带有子类型限定的通配符允许读取一个泛型对象。还可以使用根本无限定的通配符。通配符不是类型变量,因此,不能在编写代码中使用 ”?“ 作为一种类。通配符捕获只有在非常限定的情况下才是合法的。编译器必须能够保证通配符表示单个确定的类型。
泛型Class类的 API如下:
Java泛型的突出特性之一是在虚拟机中擦除泛型类型。
为了表述泛型类型声明,可以使用java.lang.reflect包中的接口Type。这个接口包含以下子类型:
1)Class类,描述具体类型。
2)TypeVariable接口,描述类型变量。
3)WildcardType接口,描述通配符。
4)ParameterizedType接口,描述泛型类或接口类型。
5)GenericArrayType接口,描述泛型数组。
Type接口的继承层次如下。注意,最后4个子类型是接口,虚拟机将实例化实现这些接口的适当的类。
CDI和Guice等注入框架使用类型字面量来控制泛型类型的注入。
反射和泛型的的部分API如下: