一、前言
泛型(Generics)是 JDK 1.5 引入的重要特性,核心思想:类型参数化。 简单理解:把数据类型当作参数传递,定义类 / 接口 / 方法时不固定类型,在使用的时候再指定具体类型。
泛型带来的 3 大好处
- 编译期类型检查 :类型错误在编译阶段就发现,而不是运行时抛出
ClassCastException; - 消除强制类型转换,代码更简洁;
- 代码复用:一套逻辑支持多种引用类型,不用重复编写相似类。
注意:Java 泛型是编译期泛型 ,依靠类型擦除实现,运行期 JVM 不保留泛型信息。这是 Java 泛型和 C++ 模板最大区别。
常用泛型标识符(约定俗成)
TType:任意类型EElement:集合元素(Collection/List)KKey:Map 键VValue:Map 值RReturn:返回值类型S,U:多个额外类型参数
二、基础语法:泛型类、泛型接口、泛型方法
1. 泛型类
定义类的时候,在类名后<T>声明类型参数。
java
public class Box<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
使用:
java
Box<String> strBox = new Box<>();
strBox.setData("hello");
Box<Integer> intBox = new Box<>();
intBox.setData(100);
2. 泛型接口
java
public interface IGenerator<T> {
T next();
}
实现方式:
- 实现类确定类型:
java
public class StringGenerator implements IGenerator<String>{
@Override
public String next() { return "test"; }
}
- 实现类继续保留泛型参数:
java
public class ObjectGenerator<T> implements IGenerator<T>{
@Override
public T next() { return null; }
}
3. 泛型方法⭐重点
泛型方法的类型参数写在方法返回值前面,和泛型类无关。
即使类不是泛型类,也可以单独定义泛型方法。
java
public class GenericUtil {
// <T> 声明泛型参数
public static <T> T getFirst(T[] arr){
if(arr == null || arr.length == 0){
return null;
}
return arr[0];
}
}
调用时编译器自动类型推断:
java
String s = GenericUtil.getFirst(new String[]{"A","B"});
区分坑:泛型类上的 T 和 泛型方法上的 T,不是同一个类型参数,名字相同也互相独立。
三、有界类型(限定泛型参数范围)
可以限制泛型参数必须是某个类或者它的子类,使用extends上界限定。
注意:
extends在这里代表上限,不仅可以是类,也可以是接口。
java
// T只能是Number或者Number的子类:Integer、Double等
public <T extends Number> double sum(T a, T b){
return a.doubleValue() + b.doubleValue();
}
- 有界之后,编译器知道 T 是 Number 子类,可以直接调用 Number 里面的方法。
- 没有
super用来限定类型参数 ,super只用于通配符。
四、通配符?(Wildcard)
通配符?代表未知类型,主要用于方法参数,提升 API 灵活性。 分三类:无界通配符、上界通配符、下界通配符。
1. 无界通配符 <?>
代表任意未知类型。
适用:只读取,不写入,不关心元素具体类型。
java
public static void printList(List<?> list){
for(Object obj : list){
System.out.println(obj);
}
}
⚠️ 限制:不能 add,除了 null。因为不知道集合真实类型,无法保证类型安全。
2. 上界通配符 <? extends T>
代表T 或者 T 的子类,协变。
java
// 可以接收 List<Integer>、List<Double>、List<Number>
public static double sumNumberList(List<? extends Number> list){
double sum = 0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
✅ 可读:取出元素一定是 Number; ❌ 不可写入:编译器不知道集合真实子类型,禁止 add,无法保证类型安全。
3. 下界通配符 <? super T>
代表T 或者 T 的父类,逆变。
java
// 接收 List<Integer>、List<Number>、List<Object>
public static void addInt(List<? super Integer> list){
list.add(100);
list.add(200);
}
✅ 可以写入Integer 及其子类; ❌ 读取的时候只能拿到 Object 类型。
五、PECS 原则(面试高频,出自 Effective Java)
PECS:Producer Extends,Consumer Super
- Producer(生产者,读取数据 ):使用
<? extends T>; - Consumer(消费者,写入数据 ):使用
<? super T>; - 既要读又要写:不要使用通配符,直接使用确定泛型
<T>。
经典例子:
Collections.copy()
java
public static <T> void copy(List<? super T> dest, List<? extends T> src)
src 是源,生产者读取 → ? extends T dest 是目标,消费者写入 → ? super T
六、类型擦除 Type Erasure【泛型底层原理,必考点】
Java 泛型的实现方式:编译期校验类型,编译完成后擦除泛型信息。运行时 JVM 看不到泛型。
擦除规则
- 无界类型参数
<T>:擦除为Object - 有界类型参数
<T extends Number>:擦除为边界类型Number - 编译器自动插入强制类型转换,保证代码正常运行。
示例:
java
List<String> list = new ArrayList<>();
list.add("test");
String s = list.get(0);
编译擦除之后,等价原始代码:
java
List list = new ArrayList();
list.add("test");
String s = (String) list.get(0);
重要结论
java
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
// true!运行时两者都是ArrayList,泛型信息被擦除
System.out.println(list1.getClass() == list2.getClass());
运行时List<String> 和 List<Integer>是同一个 Class。
桥接方法 Bridge Method
当泛型类继承 / 实现泛型接口,类型擦除会导致方法签名不一致。编译器自动生成桥接方法,保证多态正常执行。
面试追问:桥接方法作用? 答:类型擦除之后,父类原始类型方法和子类泛型方法签名不一致,编译器自动生成桥接方法,保证多态调用正常。
七、泛型的局限性(坑点,开发不能踩)
因为类型擦除,泛型存在很多限制:
- 不能使用基本类型 ,
List<int>编译报错,只能List<Integer>(装箱) - 不能 new T (),运行时 T 被擦除,不知道真实类型;
- 不能创建泛型数组
new T[10];可以创建Object[]或者使用Array.newInstance反射创建; - 不能用 instanceof 判断泛型类型
java
if(list instanceof List<String>) {} // 编译报错,泛型运行时擦除
if(list instanceof List<?>) {} // ✅合法
- 静态变量不能使用类上的泛型参数;静态方法如果要用泛型,必须单独定义静态泛型方法。
- 异常不能是泛型类,
class MyException<T> extends Exception编译报错。
八、不变、协变、逆变(数组 vs 泛型集合)
- 数组是协变 :
String[]是Object[]的子类型
java
Object[] arr = new String[5]; // 合法,但运行时存Integer会抛ArrayStoreException
- 泛型集合是不变 :
List<String>不是List<Object>的子类型!
java
List<Object> list = new ArrayList<String>(); // ❌编译直接报错
这就是为什么需要通配符
? extends / ? super来获得协变 / 逆变能力。
九、泛型开发实战避坑指南
坑 1:原始类型 Raw Type
List list = new ArrayList(),不带泛型参数就是原始类型。 原始类型跳过编译期泛型检查,容易带来ClassCastException,开发尽量不要使用。
坑 2:泛型擦除导致反射拿不到真实类型
List<String>在运行期只能拿到List,拿不到 String。Spring 中TypeReference就是用来解决这个问题,通过匿名内部类保留泛型信息。
java
// Spring 泛型捕获
TypeReference<List<String>> typeRef = new TypeReference<List<String>>(){};
坑 3:PECS 用反,导致编译报错
读取数据用 super,写入用 extends,会出现无法读 / 写编译错误。记住生产者 extends,消费者 super。
坑 4:泛型方法和泛型类参数重名混淆
类上<T>和方法<T>是两套独立参数,尽量不要同名,避免阅读混淆。
十、面试高频问答(背诵版)
Q1:Java 泛型底层原理是什么?
A:Java 泛型基于类型擦除实现。编译阶段做类型校验;编译完成后,泛型信息被擦除,无界 T 变成 Object,有界 T 变成边界类型;编译器自动插入类型强转。泛型信息只存在编译期,运行时 JVM 不存在。
Q2:List<String>是不是 List<Object>子类?
A:不是,泛型集合不变。List<String>不是 List<Object>的子类型;数组是协变。
Q3:<? extends T> 和 <? super T>区别?PECS 原则?
A:? extends T上界通配符,代表 T 或子类,生产者,可读不可写 ;? super T下界通配符,代表 T 或父类,消费者,可写,读取只能得到 Object。PECS:Producer Extends,Consumer Super。
Q4:为什么不能 new T ()?
A:类型擦除,运行期 T 变成 Object,无法获取原始类型信息,不能实例化。
Q5:桥接方法是什么?
A:类型擦除之后,父类方法签名和子类方法签名不一致,编译器自动生成桥接方法,保证多态调用正常执行。
Q6:泛型和数组协变的问题?
A:数组协变,编译不报错,运行存入错误类型抛 ArrayStoreException;泛型集合不变,编译期直接拦截,类型更安全。
十一、总结
Java 泛型通过类型参数化 实现编译期类型安全,核心底层机制是类型擦除。 泛型类、泛型接口可以把类型定义在类级别;泛型方法可以独立声明类型参数。 通配符?用来提升 API 灵活性:? extends T用于读取生产者,? super T用于写入消费者,遵循 PECS 原则。 受类型擦除限制,泛型有很多限制,不能使用基本类型、不能直接 new T ()、无法 instanceof 判断泛型参数。开发中合理使用通配符,避免原始类型,写出更安全通用的工具类。泛型大量应用在集合、Spring、MyBatis、RPC 框架中,是后端面试高频考点。