Java泛型
2023-06-22 14:51:00

泛型原理

什么是泛型&为什么引入泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(521);//添加 Integer 类型元素
list.add("wan");//添加 String 类型元素
list.add(true);//添加 Boolean 类型元素
list.add('a');//添加 Character 类型元素
Object item1 = list.get(0);//只能用 Object 接受元素
list.forEach(item -> {
//使用 item,这里的 item 类型是 Object,由于不知道 item 的确切类型,我们需要判断之后强转
if (item instanceof Integer) {
//执行业务...
} else if (item instanceof String) {
//执行业务...
} else if (item instanceof Boolean) {
//执行业务...
} //继续判断类型...
});
}

JDK5 之前没有泛型时,声明的 List 集合默认是可以存储任意类型元素。开发的时候由于不知道集合中元素的确切类型,遍历的时候我们拿到的 item 其实是 Object 类型,如果要使用就必须强转,强转就必须得判断当前元素的具体类型,否则直接使用强转很可能会发生类型转换异常。这样就会让开发很不方便,每次都要额外做判断工作。

有了泛型的指定,我们声明的 list 就只能存储规定类型 String ,当存储其他类型的元素时编辑器就会直接给我们报错(可以在 IDEA 开发环境中看 add 方法提示参数类型就是 String),这样类型不匹配的问题就在编译时候就能检查出来,而不会在运行时才抛出异常。而且当我们进行遍历、获取元素等操作时,get 方法返回值就是 String 类型的。

总结泛型的好处

  • 编译期类型安全
  • 避免了强制类型转换运行时异常
  • 同一个类可以操作多种类型数据,代码复用

泛型类

ArrayList 是最典型的泛型类例子。 ArrayList 在定义的时候指定了一个泛型,并且下面的添加元素、获取元素等方法也都是对这个 E 进行操作。也可以在一个类中定义多个泛型参数,比如 HashMap。

1
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>

泛型方法

有时候开发中我们会有这样一种需求,根据方法传入的参数类型不同,返回不同的返回值类型。

1
2
3
4
5
6
7
8
9
10
11
public static <E, T> List<T> convertListToList(List<E> input, Class<T> clzz) {
List<T> output = Lists.newArrayList();
if (CollectionUtils.isNotEmpty(input)) {
input.forEach(source -> {
T target = BeanUtils.instantiate(clzz);
BeanUtils.copyProperties(source, target);
output.add(target);
});
}
return output;
}

无界泛型通配符

“?” 代表不确定的类型。泛型的上界下界:

1
2
IPage<? extends AppOrderResponse> //表示泛型最高类型是 AppOrderResponse,只能是它或它的子类
IPage<? super AppOrderResponse> //表示泛型最低类型是 AppOrderResponse。只能是它或它的父类

上面都是把无界通配符用在返回值,无界通配符也是可以用在方法参数上的。

使用上界参数

1
2
3
4
5
6
 public void test1(List<? extends AppOrderResponse> list){
list.add(new AppOrderDispatchedResponse());//添加元素报错,因为我们传入的可能是 List<AppOrderWaitPaidResponse>
list.add(new AppOrderResponse());//添加元素报错,因为我们传入的可能是 List<AppOrderWaitPaidResponse>
list.add(null);//这是唯一可以添加的元素 null
AppOrderResponse appOrderResponse = list.get(0);//接受元素类型为 AppOrderResponse
}

泛型上界参数在方法内只能读取,不能写入。

使用下界参数

1
2
3
4
5
public void test2(List<? super AppOrderResponse> list){
list.add(new AppOrderResponse());//可以添加
list.add(new AppOrderDispatchedResponse());//可以添加
Object object = list.get(0);//接受元素的类型为 Object
}

泛型下界参数在方法内只能写入,不能读取。

泛型通配符 “?” 和 T、E、R、K、V 的区别

  • T、E、R、K、V 对于程序运行时没有区别,只是名字不同
  • ? 表示不确定的泛型类型
  • T (type) 表示具体的一个泛型类型
  • K V (key value) 分别代表 Map 中的键值 Key Value
  • E (element) 代表元素,例如 ArrayList 中的元素
  • T 用于定义泛型类和泛型方法
  • “?” 用于方法形参

泛型擦除

所谓的泛型擦除其实很简单,简单来说就是泛型只在编译时起作用,运行时泛型还是被当成 Object 来处理,示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("wan");//add 方法形参类型为 String
String s = list.get(0);//get方法返回值类型为 String
ArrayList<Integer> list2 = new ArrayList<>();
System.out.println("list 和 list2 类型相同吗:" + (list.getClass() == list2.getClass()));//true 两个 ArrayList 是同一个类型的
Method[] methods = list.getClass().getMethods();
for (Method method : methods) {
method.setAccessible(true);
if (method.getName().equals("add")) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {
for (Class<?> parameterType : parameterTypes) {
System.out.println("add(E e) 形参 E 的类型为:" + parameterType.getName());//泛型的参数 E 运行时是 Object 类型
}
}
} else if (method.getName().equals("get")) {
Class<?> returnType = method.getReturnType();
System.out.println("E get(int index) 的返回值 E 的类型为:" + returnType.getName());
}
}
}

可以看到我们实例化 ArrayList 时虽然传入不同的泛型,但其实它们仍然还是同一个类型。对于 add 方法的形参和 get 方法的返回值,按道理说我们指定的泛型是 String 那么打印出来应该是 String 才对,但是这里运行时得到的却都是 Object,所以这就足以证明了,泛型在编译期起作用,运行时一律被擦除当做 Object 看待,这就是泛型擦除。

参考资料

  1. 一文带你搞懂 Java 泛型 - 知乎 (zhihu.com)
  2. Java 泛型擦除 - hongdada - 博客园 (cnblogs.com)