前言
泛型是 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 extend String> { }
如果一个类中存在多个泛型,泛型的名称之间可以用英文逗号分隔。示例如下。
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 中泛型的简化。