我们日常中几乎每天都会用到泛型,那么泛型的原理是怎么样的呢,从官网中可以看到,泛型的介绍有两大类,通配符和泛型方法
接下来我们来搞懂什么以下几个知识点:
1、 什么是类型擦除
2、常用的 ?, T, E, K, V, N的含义
3、上界通配符 < ?extends E>
4、下界通配符 < ?super E>
5、什么是PECS原则
6、通过一个案例来理解 ?和 T 和 Object 的区别
一:什么是类型擦除
我们说Java的泛型是伪泛型,那是因为泛型信息只存在于代码编译阶段,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程为类型擦除。
@Test
void testSomeMethod() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 定义long类型的集合
ArrayList<Long> list = new ArrayList<>();
list.add(1L);
// 定义class对象,下面的方法就会把原始的long类型的的集合给擦除
Class<? extends ArrayList> clazz = ArrayList.class;
Method add = clazz.getDeclaredMethod("add", Object.class);
add.invoke(list, "测试类型擦除");
System.err.println(list);
}
返回结果: [1, 测试类型擦除]
二:常用的 ?, T, E, K, V, N的含义
对于常见的这几种类型其实没有什么特殊的含义,只不过是由一些命名规范,例如以下
- T (Type) 具体的Java类
- E (Element)在集合中使用,因为集合中存放的是元素
- K V (key value) 分别代表java键值中的Key Value
- N (Number)数值类型
- ? 表示不确定的 Java 类型
三:上界通配符 < ?extends E>
举例: <? extends ArrayList> 主要用于只读操作(即“生产者”场景,Producer)。比如你只想从一个集合中读取元素,而不往里面添加。
import java.util.*;
public class Demo {
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
printNumbers(intList); // OK
printNumbers(doubleList); // OK
}
}
四:下界通配符 < ?super E>
举例 :<? super Test> 主要用于写入(添加)元素,也就是“消费者”场景(Consumer)。因为你知道集合至少能接受 T 类型及其子类。
import java.util.*;
public class Demo {
public static void addIntegers(List<? super Integer> list) {
list.add(1); // OK
list.add(100); // OK
// list.add(3.14); // 编译错误,Double 不是 Integer 的子类
}
public static void main(String[] args) {
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addIntegers(numbers); // OK
addIntegers(objects); // OK
}
}
五:什么是PECS原则
Producer - extends(只读)
Consumer - super(可写)
六:通过一个案例来理解 ?和 T 和 Object 的区别
1. 使用 Object
public static void processObject(List<Object> list) {
list.add("new item"); // ✅ 允许添加
Object obj = list.get(0); // ✅ 返回 Object
System.out.println("Object: " + obj);
}
2. 使用 <?>
public static void processWildcard(List<?> list) {
// list.add("new item"); ❌ 不允许添加(除了 null)
Object obj = list.get(0); // ✅ 可以读取为 Object
System.out.println("Wildcard: " + obj);
}
3. 使用泛型 <T>
public static <T> void processGeneric(List<T> list) {
T item = list.get(0); // ✅ 返回 T 类型
list.add(item); // ✅ 允许添加 T 类型
System.out.println("Generic: " + item.getClass().getSimpleName());
}
注意:在 Java 中,泛型方法本身是不能仅凭类型参数的不同进行重载的。这是因为 Java 编译器在编译期间会进行类型擦除(Type Erasure) ,在字节码层面泛型信息是不存在的,因此如果两个方法在类型擦除后具有相同的签名,就会发生冲突。