1. 泛型的概念
泛型,即“参数化类型”。就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(类型形参),然后在使用/调用时传入具体的类型(类型实参)。
- JDK1.5 之后引入。
- 让代码更通用更灵活。
- 核心目标是解决容器类型在编译时安全检查的问题。
在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
2. 泛型的使用
类型参数(类型变量)用作占位符,可能有一个或多个,作用于整个类。根据惯例类型参数是单个大写字母:
- E:元素,一般在线性结构集合中使用
- K:键,一般在键值类型的集合中用于表示键
- V:值,一般在键值类型的集合中用于表示值
- N:数字
- T:类型
2.1. 泛型类/接口
class 类名称<类型参数1, 类型参数2 ...> {}
interface 接口名称<类型参数1, 类型参数2 ...> {}
// 泛型类public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {}// 泛型接口public interface Map<K, V> {}
2.2. 泛型方法
类型参数0(返回值类型) 方法名称<类型参数1 参数1, 类型参数2 参数2 ...> {}
public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}
注意:不能在静态方法、静态初块等静态内容中使用泛型的类型参数。
public class A<T> {public static void func(T t) {//报错,编译不通过}}
2.3. 通配符
问号通配符 ? 表示未知类型。通配符可用于参数、字段、局部变量和返回类型。可以近似的理解为泛型的泛型。
- 通配符匹配出来的泛型类型只能读取,不能写入。因为不知道这个容器放什么类型的数据,所以只能读取不能添加。
- 最好不要在返回类型中使用通配符,因为确切知道方法返回的类型更安全。
public static void main(String[] args) {List<String> name = new ArrayList<String>();List<Integer> age = new ArrayList<Integer>();List<Number> number = new ArrayList<Number>();name.add("icon");age.add(18);number.add(314);getData(name); // icongetData(age); // 18getData(number); // 314}// List<?>,在逻辑上是List<String>,List<Integer>等所有List<具体类型实参>的父类。public static void getData(List<?> data) {System.out.println("data :" + data.get(0));}
2.4. 上下边界限定
- 上界限定通配符:
<? extends E>,表示只接受E类型及其子类型。 - 下界限定通配符:
<? super E>, 表示只接受E类型及其父类型。
public static void main(String[] args) {List<String> name = new ArrayList<String>();List<Integer> age = new ArrayList<Integer>();List<Number> number = new ArrayList<Number>();name.add("icon");age.add(18);number.add(314);getUperNumber(name); // 编译时报错,String类型不是Number的子类getUperNumber(age); // 18getUperNumber(number); // 314}public static void getData(List<?> data) {System.out.println("data :" + data.get(0));}// List<? extends Number>,在逻辑上是List<Number>,List<Integer>等类的父类。public static void getUperNumber(List<? extends Number> data) {System.out.println("data :" + data.get(0));}
3. 泛型的原理
Java 语言的泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。
3.1. 类型擦除
泛型信息只存在于编译前,编译后的字节码中是不包含泛型中的类型信息的。因此,编译器在编译时去掉类型参数,叫做类型擦除。
例如 List<Integer> 和 List<String> 等类型在编译之后都会变成 List。JVM看到的只是 List,而泛型信息对JVM来说是不可见的。
Class c1 = new ArrayList<String>().getClass();Class c2 = new ArrayList<Integer>().getClass();System.out.println(c1 == c2); // true
3.1.1. 未指定上界的泛型类型会以Object类型替换
// 泛型类java源代码public class Test<T> {private T test;}// 编译后的代码public class Test{public Test(){}private Object test; // 原来的泛型T被替换为Object}
3.1.2. 已指定上界的泛型类型会以上界类型替换
// 泛型类java源代码public class Test<T extends String> {private T num;}// 编译后的代码public class Test{public Test(){}private String test; // 原来的泛型T被替换为上界类型String}
3.2. 绕过编译时泛型类型检查
List<Integer> list = new ArrayList<>();list.add(123); // 正常编译list.add("string"); // 编译报错【不兼容的类型: java.lang.String无法转换为java.lang.Integer】
基于类型擦除,我们可以利用反射绕过这个限制。
List<Integer> list = new ArrayList<>();list.add(123);try {// 由于List中的泛型参数没有设置上界,所以add方法可以add任何Object的子类型参数Method method = list.getClass().getDeclaredMethod("add", Object.class);method.invoke(list, "string");method.invoke(list, true);method.invoke(list, 45.6);} catch (Exception e) {e.printStackTrace();}
