Java 泛型笔记:类型参数、通配符与类型擦除
📝 笔记摘要:Java 泛型,编译期就把类型错误拦住
泛型解决的是一个具体问题:List 里塞进去什么类型,取出来的时候编译器不知道,只能靠强转硬扛,转错了要等到运行时才炸。泛型把这个「不知道」写进了类型里,编译期就能查。
这篇分五节:泛型是什么、泛型类和接口、泛型方法、通配符、类型擦除。类型擦除是最后一节,也是最容易漏的一节——它决定了为什么泛型类里不能 new T[],也决定了反射读泛型为什么要绕一圈。基础语法在 Java 基础笔记 里铺过,集合里 List<E> 怎么用,Java 集合笔记 那篇拆得细。
上篇 类型系统
1. 什么是泛型 2. 泛型类与泛型接口
3. 泛型方法
下篇 通配符与擦除
4. 泛型通配符 5. 类型擦除
1. 什么是泛型
泛型是 JDK 5 引入的特性,做的事只有一件:把类型当成参数用。写 List<String> 的时候,String 不是随手写的备注,它是一个类型参数,编译器拿它来检查这个容器里到底允许放什么。
没有泛型的时候,编译器只知道容器里装的是 Object:往里塞什么都行,取出来只能强转,转错了要等运行到那一行才抛 ClassCastException。泛型把这道防线从运行时提到了编译期。
// 不带泛型:放进去没人拦,取出来要自己赌
List list = new ArrayList();
list.add("abc");
String s = (String) list.get(0); // 编译通过,运行到这行才炸 ClassCastException
// 带泛型:类型写在声明里,编译器替你盯着
List<String> names = new ArrayList<>();
names.add("abc");
names.add(123); // 编译期就报错,压根跑不起来
泛型参数只能是引用类型
List<int>编译不过,要写List<Integer>
八个基本类型对应八个包装类,泛型系统里只认包装类
自动装箱只在赋值和传参时发生,容器里实际存的是包装类对象
泛型是编译期的,运行时什么都不剩
反射拿到的List里全是Object,元素类型在字节码里被抹掉了
这条代价单独占一节,见第 5 节
2. 泛型类与泛型接口
泛型类就是把类型参数写在类名后面,类里的字段、方法、构造器都能用这个类型参数。类型参数的名字只是约定,写 T 还是 E 还是 K 都不影响编译,但团队里最好统一:T 给类型、E 给元素、K/V 给键值对。
// 类名后面加尖括号,里面是类型参数列表
public class Box<T> {
private T value; // 字段用 T
public Box(T value) {
this.value = value;
}
public T get() { // 返回值用 T
return value;
}
public void set(T value) {
this.value = value;
}
}
接口写法完全一样,只是关键字换成 interface:
public interface Provider<T> {
T get();
}
public class StringProvider implements Provider<String> {
@Override
public String get() {
return "hello";
}
}
泛型类之间继承时有个硬规则:子类必须把父类的类型参数接住。要么原样传下去,要么在父类那里就定死成一个具体类型。
// 父类的 T 由子类接住,子类可以再加自己的类型参数
public class ParentBox<T> {
}
public class ChildBox<T, E> extends ParentBox<T> {
}
// 子类不需要泛型,就要在父类那里写死
public class FixedBox extends ParentBox<String> {
}
泛型类里不能有「用类类型参数」的静态成员
static T value;和static T get() { }都编译不过
静态成员属于类,不属于某个实例,而T是每个实例自己的类型
绕法是让静态方法自己声明类型参数:public static <T> T of(T v)
泛型方法可以加 static
方法和类一样是静态的,跟实例没关系,不冲突
菱形语法new Box<>()只在 new 的时候用
声明变量那侧要写全:Box<String> b = new Box<>();
3. 泛型方法
泛型方法是方法自己带类型参数,写在修饰符后面、返回值类型前面。它跟类的泛型是两套独立的东西:类上的 T 在 new 的时候就定死了,方法上的 <T> 是每次调用时按实参推断出来的。
public class Utils {
// 泛型方法:<T> 写在 public 后面、返回值前面
public static <T> T first(List<T> list) {
return list.get(0);
}
// 调用时不用写 <T>,编译器从实参推断
List<String> names = Arrays.asList("a", "b");
String first = Utils.first(names); // T 推断成 String
List<Integer> nums = Arrays.asList(1, 2);
Integer n = Utils.first(nums); // T 推断成 Integer
}
同一个类里,类级泛型和方法级泛型可以并存,名字甚至可以都叫 T,它们互不干扰。
public class Holder<T> {
private T field;
// 类里的普通方法用的是类的 T
public T getField() {
return field;
}
// 静态方法只能用自己声明的 T
public static <E> E pick(E a, E b) {
return a;
}
}
泛型方法的位置记牢
public static <T> T method(...),尖括号夹在修饰符和返回值之间
写成public T method(...)的,那是类级泛型,不是泛型方法
类型推断能推出来就别手写
Utils.<String>first(names)是合法但多余的写法
只有实参推不出来时才需要显式指定,比如传的是null
返回值是泛型时,调用方不需要强转
方法签名已经把类型说清楚了,String first = Utils.first(names);直接赋值
4. 泛型通配符
通配符是给「不关心具体类型」的场合用的。尖括号里写死 String,这个容器就只能是 String 的容器;写 ?,就表示「里面是什么都行,我只负责传」。
三种写法,区别在能不能读、能不能写:

?任意类型
只能读出Object,不能写(写什么都可能类型不匹配)
? extends T上界通配符
只能是T或者T的子类,只读不写
? super T下界通配符
可以是T或者T的父类,只写不读
一句话记法:Producer Extends,Consumer Super
生产者往外掏东西用extends,消费者往里塞东西用super
public class WildcardDemo {
static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) { // 读:Number 及其子类都能当 Number 用
total += n.doubleValue();
}
return total;
}
static void fill(List<? super Integer> list) {
list.add(1); // 写:Integer 一定在 ? super Integer 的范围内
list.add(2);
}
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
List<Double> doubles = new ArrayList<>(List.of(1.5, 2.5));
System.out.println(sum(ints)); // 6.0
System.out.println(sum(doubles)); // 4.0
List<Object> objects = new ArrayList<>();
fill(objects);
System.out.println(objects); // [1, 2]
}
}
这个「只读不写」「只写不读」不是编译器刁难,是类型系统逼出来的:List<? extends Number> 里实际装的可能是 List<Integer>,往里塞一个 Double 就把人家的类型承诺毁了。
上界通配符取出来只能当父类用
List<? extends Number>里取出的元素,类型只能是Number
想拿到Integer就得自己强转,编译器不给你这个承诺
下界通配符取出来只能是Object
List<? super Integer>可能是List<Object>,取出时只能是Object
直接写具体类型最省事就别用通配符
只有「方法要同时兼容多种类型」时才需要?
一个泛型方法想同时接收List<Integer>和List<Double>,就写<? extends Number>
5. 类型擦除
泛型只活在编译期。编译完之后,Box<String> 和 Box<Integer> 在字节码里是同一个类 Box,类型参数被抹成了它的上界(没写上界就是 Object)。这个机制叫类型擦除。

所以运行时拿到的 Class 对象,看不出元素类型;编译器自己解决类型问题靠的是在调用点插入强转和桥接方法。
// 源码里写的是这个
Box<String> box = new Box<>("hi");
String v = box.get();
// 编译后大致是这个(编译器插入的强转)
Box box = new Box("hi");
String v = (String) box.get(); // 这里就是运行期 ClassCastException 的现场
类型擦除带来三条硬限制,写代码时会直接撞上:
public class ErasureLimit {
public static <T> T[] newArray(int size) {
// 编译不过:不能 new T[],运行时没人知道 T 是多长
// return new T[size];
Object[] tmp = new Object[size]; // 折中办法
return (T[]) tmp; // 这里同样编译不过,要加 @SuppressWarnings
}
public static void main(String[] args) {
Object o = "hello";
// if (o instanceof String) {} // 编译不过:运行时 T 没了
if (o instanceof String) { // 只能写成具体类型
System.out.println(((String) o).length());
}
}
}
反射想在运行时把泛型信息捞回来,得走另一套 API:getGenericParameterTypes() 和 getGenericReturnType() 拿到的不是 Class 而是 Type,得判断是不是 ParameterizedType 再取真实参数。
Method method = SomeClass.class.getMethod("find", Map.class);
Type type = method.getGenericParameterTypes()[0]; // java.util.Map<java.lang.String, X>
if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType) type;
Type[] args = p.getActualTypeArguments(); // [String, X]
}
擦除的直接后果:
instanceof不能判断泛型
list instanceof List<String>编译不过
运行时只分得出List和ArrayList,分不出元素类型
擦除的第二条后果:new T[]不能写
想造泛型数组,只能走(T[]) new Object[n]并自己保证类型安全
擦除的第三条后果:单例泛型类的静态字段不能共用
class Cache<T> { static T item; }编译不过,理由见第 2 节
JSON 反序列化靠泛型信息定位字段类型
擦除让框架读不到List<User>里的User,Jackson 这类库要靠TypeReference把泛型类型显式留下来
反射读泛型的完整写法见 Java 注解与反射
💡 补充几个容易忽略的:
泛型不支持基本类型,也不支持数组
List<int[]>是非法的,数组本身就是对象,List<String[]>才是合法的
泛型方法的类型参数可以自己声明上界
<T extends Comparable<T>>这种写法在做排序工具类时很常见
桥接方法是编译器生成的,不是你写的
子类重写泛型父类方法时,编译器会补一个桥接方法保证多态能正常走,看到它在栈里出现不用慌
通配符和泛型方法能解决的问题不同
通配符解决「参数类型兼容」,泛型方法解决「返回值类型精确」,两者可以同时用
读泛型源码之前先把集合那篇啃下来
Java 集合笔记里的List<E>、Map<K, V>全是泛型,不懂泛型就看不懂集合的签名
🔥 一句话总结:泛型是编译期的类型检查,容器写什么类型编译器就管什么类型;泛型类把类型参数写在类名后面,泛型方法把类型参数写在修饰符和返回值之间,两者互相独立;通配符三条规则——
?只能读成Object,? extends T只读不写,? super T只写不读,记法是 Producer Extends、Consumer Super;类型擦除决定了泛型类里不能有static T、不能new T[]、不能instanceof T,反射读泛型得靠ParameterizedType。
