前言
泛型是 Java 5 新增的一项特性,可以理解为类型的参数,主要用于代码重用,语义化代码,避免运行时的强制类型转换异常。
在泛型出现之前,集合中的 List 存储的对象只能为 Object,示例代码如下
List list = new ArrayList();
list.add("str");
Integer num = (Integer)list.get(0);
从 List 中获取 Integer 类型的对象,需要进行强制类型转换,如果不能保证存储的对象只为 Integer 类型,很容易出现 ClassCastException。泛型出现后上述代码可修改为如下。
List<Integer> list = new ArrayList<>();
list.add("str"); // 编译时报错
Integer num = list.get(0);
修改后的代码中,List 类型后携带了<Integer>
,表示 List 中存储的只能为 Integer 类型,此时如果向 List 中添加其他类型,则会在编译时报错,将运行时的类型检查提前到编译期,避免的错误的产生,同时语义也相对清晰,一眼可以看出 List 中存储的是什么类型。
泛型的使用
泛型类及泛型方法
使用泛型,首先需要进行定义,泛型可以用在类上和方法上。
泛型在类上面的定义,只需要在类后面添加尖括号,然后在尖括号中为泛型取一个名字即可,一般为比较简短的大写英文字母,常用的 T 表示任意类型,E 表示集合中的元素,K 和 V 分别表示键和值。示例如下。
public class GenericClazz<T> {
}
如果一个类中存在多个泛型,泛型的名称之间可以用英文逗号分隔。示例如下。
public class GenericClazz<K,V> {
}
泛型可以用来表示任意类型,如果我们想要限制泛型的类型,则需要使用 extend 表示泛型只能为某个接口或类的子类。示例如下。
public class GenericClazz<T extend String> {
}
此时 T 只能用于表示字符串类型,如果想表示多个接口的子类,则可以在类型之间使用 & 符合连接。示例如下。
public class GenericClazz<T extend String & Serializable> {
}
泛型定义后,一般我们会在成员变量或方法中使用,如下所示。
public class GenericClazz<T extend String> {
private T param;
public T getParam(){
return this.param;
}
public void setParam(T param){
this.param = param;
}
}
使用方式如下。
GenericClazz<String> clzss = new GenericClazz();
clazz.setParam("abc");
String param = clazz.getParam();
除了在类上定义泛型,还可以直接在方法上定义泛型。在方法上定义泛型需要在方法的修饰符后,返回值前定义泛型类型,如下所示。
public class Test {
public static <T> T getParam(T param) {
return param;
}
}
调用泛型方法的示例如下。
public class Test {
public static void main(String[] args) {
String param = Test.getParam("param");
}
}
类型擦除
每个泛型通过编译都会转换为一个原始类型,没有 extend 限制的泛型对应的原始类型是 Object,有 extend 限制的泛型类型为 extend 后面的第一个类型。如下
public class GenericClazz<T extend String> {
private T param;
}
上述中的代码在编译后可能会转换为如下。
public class GenericClazz{
private String param;
}
也就是说,泛型是通过类型擦除实现的,编译后的 class 文件中泛型已经转换为了具体的类型,由于存在类型擦除,编译器可能会插入强制类型转换的代码或生成桥接方法。
如下代码所示。
public class GenericClazz<T> {
private T param;
public T getParam() {
return this.param;
}
public static void main(String[] args) {
GenericClazz<Integer> clazz = new GenericClazz<>();
Integer param = clazz.getParam();
}
}
泛型类型 T 经过类型擦除,getParam 方法返回的类型会转换为 Object 类型,示例代码将其返回值赋值给 Integer 类型的变量,因此编译器会在赋值的指令中插入强制类型转换的代码。
如果类型擦除和多态发生冲突,编译器则会自动生成桥接方法,看下面的代码。
public class GenericClazz<T> {
private T param;
public T getParam() {
return this.param;
}
public void setParam(T param) {
this.param = param;
}
public static void main(String[] args) {
SubGenericClazz subGenericClazz = new SubGenericClazz();
subGenericClazz.setParam("str");
}
}
class SubGenericClazz extends GenericClazz<String> {
@Override
public void setParam(String param) {
return super.setParam(param);
}
}
不带泛型的类 SubGenericClazz
继承了泛型类 GenericClazz<String>
,然后实现其方法,然后将子类赋值给父类的引用,由于多态的存在,调用父类的方法时将会调用实际类型的方法,而父类由于类型擦除,最终调用的方法应该为 GenericClazz#setParam(Object param)
,而子类 SubGenericClazz
并不存在这样的方法,此时类型擦除和多态发生冲突,编译器自动生成桥接方法 SubGenericClazz#setParam(Object param)
,生成的代码可以理解如下。
class SubGenericClazz extends GenericClazz<String> {
// 生成的桥接方法
public void setParam(Object param){
return this.setParam((String)param);
}
@Override
public void setParam(String param) {
return super.setParam(param);
}
}
通配符类型
相同的类型,如果其泛型类型不同,则赋值会编译失败,如下所示。
GenericClazz<Number> genericClazz = new GenericClazz<Integer>();
这里 GenericClazz<Number>
和 GenericClazz<Integer>
,虽然都是 GenericClazz
,但由于编译时对泛型类型的检查,因此会编译失败,为了解决这个问题,可以使用通配符类型。
通配符类型使用 ?
表示,对其类型的限制除了使用 extends,还可以使用 super。上述代码修正后如下。
GenericClazz<? extends Number> genericClazz = new GenericClazz<Integer>();
extends 后面的表示通配符的上界,super 表示通配符的下界,如GenericClazz<? super Integer>
表示类型只能为 Integer 的父类,通配符如果存在上界或下界,将会影响包含通配符的对象的赋值,方法可传入的参数类型、方法的的返回值类型等。
通配符设置上界示例代码如下。
GenericClazz<? extends Number> genericClazz = new GenericClazz<Integer>();
Number param = genericClazz.getParam();
genericClazz.setParam(Integer.valueOf("1")); //编译失败
为通配符提供上界,则泛型类型作为返回值时只能返回上界的类型,而泛型类型则无法作为参数调用方法。
通配符设置下界示例代码如下。
GenericClazz<? super Integer> genericClazz = new GenericClazz<>();
Object param = genericClazz.getParam();
genericClazz.setParam(1);
为通配符设置下界后,泛型类型作为方法的返回类型只能返回 Object 类型,同时也只能使用通配符的下界类型作为方法参数的类型。
可以使用一个无上界和下界的通配符类型,此时和普通的泛型类型相比,泛型方法的返回值只能为 Object 类型,而通配符类型则无法作为方法的参数调用。
泛型与反射
虽然泛型通过类型擦除实现,但是编译后的 class 文件中仍保留着类或方法的泛型信息,在前面的文章 Java 基础知识之 Java 反射 主要将重点放在反射对类型的抽象上,反射同样提供了获取类的泛型信息的能力。
泛型自 Java 5 诞生,为了描述泛型信息,Java 将 Class 类作为类的原始类型抽象,然后又添加了一些其他的表示泛型的类型。如下图所示。
- Type:表示 Java 的某一种类型。
- WidcardType:通配符类型,如
GenericClazz<? super Integer>
中的? super Integer
。 - Class:不包含泛型信息的原始类型。
- ParameterizedType:参数化类型,如
public class GenericClazz<T extend Number> {}
中的GenericClazz<T extend Number>
。 - GenericArrayType:泛型数组类型,如
T[]
。 - TypeVariable:类型变量,如
public class GenericClazz<T extend Number> {}
中的T extend Number
。
关于反射中有关泛型的 API ,使用示例如下所示。
Class<?> clazz = String.class;
// 获取类的类型变量
TypeVariable<? extends Class<?>>[] typeParameters = clazz.getTypeParameters();
// 获取类的泛型父类
Type genericSuperclass = clazz.getGenericSuperclass();
// 获取类的泛型接口
Type[] genericInterfaces = clazz.getGenericInterfaces();
for (Field field : clazz.getDeclaredFields()) {
// 获取成员变量的泛型类型
Type genericType = field.getGenericType();
}
for (Method method : clazz.getDeclaredMethods()) {
// 获取方法返回的泛型类型
Type genericReturnType = method.getGenericReturnType();
// 获取参数的泛型类型
Type[] genericParameterTypes = method.getGenericParameterTypes();
}
TypeVariable<?> typeVariable = null;
// 获取类型参数的子类限定
Type[] bounds = typeVariable.getBounds();
WildcardType wildcardType = null;
// 获取通配符类型的上界
Type[] upperBounds = wildcardType.getUpperBounds();
// 获取通配符类型的下界
Type[] lowerBounds = wildcardType.getLowerBounds();
ParameterizedType parameterizedType = null;
// 获取参数化类型的原始类型
Type rawType = parameterizedType.getRawType();
// 获取参数化类型中泛型的真实类型
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
GenericArrayType genericArrayType = null;
// 获取泛型数组的元素类型
Type genericComponentType = genericArrayType.getGenericComponentType();
总结
泛型是 Java 中的基础知识,日常开发中,定义泛型类的场景相对较少一些,在集合中使用相对较多,泛型是学好 Java 必须掌握的技能,后面将介绍 Spring 对 Java 中泛型的简化。