泛型(Generic)是 Java 5 引入的核心特性,核心目标是将类型参数化,实现「编写一次,适配多种类型」,同时在编译期检查类型安全,避免强制类型转换(减少 ClassCastException)。本文从「基础用法→核心原理→高级特性→常见坑点」全维度拆解,让你彻底掌握泛型。
一、泛型的核心价值
1. 解决的痛点(无泛型的问题)
java
运行
// 无泛型的 ArrayList:存储任意类型,取出需强制转换,易出错
List list = new ArrayList();
list.add("Java");
list.add(123);
// 运行时才会报错(ClassCastException)
String s = (String) list.get(1);
2. 泛型的优势
java
运行
// 有泛型:编译期限制类型,无需强制转换,更安全
List<String> list = new ArrayList<>();
list.add("Java");
// 编译期直接报错(类型不匹配),提前规避问题
// list.add(123);
String s = list.get(0); // 无需强制转换
核心价值总结:
- 类型安全:编译期检查类型,避免运行时类型转换异常;
- 代码复用:一套逻辑适配多种类型(如 ArrayList 可存 String/Integer 等);
- 可读性提升:代码中明确标注类型,语义更清晰。
二、泛型的基础用法
1. 泛型类 / 接口
定义语法
java
运行
// 泛型类:<T> 是类型参数(占位符),T 可替换为任意引用类型(String/Integer/自定义类)
public class GenericClass<T> {
// 泛型成员变量
private T data;
// 泛型构造方法
public GenericClass(T data) {
this.data = data;
}
// 泛型方法
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
// 泛型接口
public interface GenericInterface<T> {
T process(T input);
}
使用示例
java
运行
// 实例化时指定具体类型:T = String
GenericClass<String> stringClass = new GenericClass<>("Hello Generic");
System.out.println(stringClass.getData()); // Hello Generic
// T = Integer
GenericClass<Integer> intClass = new GenericClass<>(100);
System.out.println(intClass.getData()); // 100
// 实现泛型接口(指定类型)
class StringProcessor implements GenericInterface<String> {
@Override
public String process(String input) {
return input.toUpperCase();
}
}
StringProcessor processor = new StringProcessor();
System.out.println(processor.process("java")); // JAVA
2. 泛型方法
定义语法(关键:<T> 放在返回值前)
java
运行
public class GenericMethodDemo {
// 泛型方法:<T> 标识这是泛型方法,T 是类型参数
public <T> T getFirstElement(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[0];
}
// 静态泛型方法(同理,<T> 必须加)
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
}
}
使用示例
java
运行
GenericMethodDemo demo = new GenericMethodDemo();
// 自动推导 T = String
String[] strArray = {"a", "b", "c"};
System.out.println(demo.getFirstElement(strArray)); // a
// 自动推导 T = Integer
Integer[] intArray = {1, 2, 3};
demo.printArray(intArray); // 1 2 3
3. 泛型通配符(?)
泛型通配符用于「不确定具体类型」的场景,核心分 3 种:
表格
| 通配符 | 含义 | 示例 |
|---|---|---|
? |
任意类型(无界通配符) | List<?> 可接收任意泛型 List |
? extends T |
上限通配符:T 或 T 的子类 | List<? extends Number> 可接收 List<Integer>/List<Double> |
? super T |
下限通配符:T 或 T 的父类 | List<? super Integer> 可接收 List<Number>/List<Object> |
示例:通配符的使用
java
运行
// 无界通配符:接收任意类型的 List,但只能读,不能写(除了 null)
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.print(obj + " ");
}
}
// 上限通配符:只能读取 Number 及其子类,不能写入(无法确定具体子类)
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) {
total += num.doubleValue();
}
return total;
}
// 下限通配符:只能写入 Integer 及其子类,读取只能是 Object
public static void addInteger(List<? super Integer> list) {
list.add(10);
list.add(20);
// 读取时只能转 Object
Object obj = list.get(0);
}
// 测试
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1,2,3);
List<Double> doubleList = Arrays.asList(1.1,2.2);
printList(intList); // 1 2 3
System.out.println(sum(doubleList)); // 3.3
addInteger(new ArrayList<Number>()); // 合法
}
三、泛型的核心原理:类型擦除
1. 什么是类型擦除?
Java 泛型是「编译期特性」,JVM 运行时不存在泛型类型信息,编译器会将泛型代码「擦除」为原生类型(Raw Type),具体规则:
- 擦除所有类型参数,替换为上限类型(无上限则替换为 Object);
- 插入必要的强制类型转换;
- 生成桥接方法(处理泛型继承 / 重写)。
2. 类型擦除示例
编译前代码
java
运行
List<String> strList = new ArrayList<>();
strList.add("Java");
String s = strList.get(0);
List<Integer> intList = new ArrayList<>();
intList.add(123);
Integer i = intList.get(0);
编译后(类型擦除)代码
java
运行
List strList = new ArrayList();
strList.add("Java");
// 插入强制类型转换
String s = (String) strList.get(0);
List intList = new ArrayList();
intList.add(123);
// 插入强制类型转换
Integer i = (Integer) intList.get(0);
3. 类型擦除的关键结论
-
运行时无泛型 :JVM 中
List<String>和List<Integer>是同一个类型(List),可通过反射验证:java
运行
List<String> strList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); // 输出 true,运行时类型相同 System.out.println(strList.getClass() == intList.getClass()); -
泛型不能用于静态变量 :静态变量属于类,类型擦除后无法区分泛型类型:
java
运行
public class Test<T> { // 编译报错:静态变量不能引用泛型类型参数 // private static T data; } -
泛型不能用于数组创建 :类型擦除后数组类型无法确定,编译报错:
java
运行
// 编译报错 // List<String>[] strArray = new List<String>[10]; // 合法(无泛型数组) List[] strArray = new List[10];
四、泛型的高级特性
1. 泛型上限(<T extends 类型>)
限制泛型类型必须是指定类型或其子类,支持多上限(类在前,接口在后,用 & 分隔)。
示例:单上限 + 多上限
java
运行
// 单上限:T 必须是 Number 或其子类
public class NumberBox<T extends Number> {
private T value;
public double getDoubleValue() {
return value.doubleValue();
}
}
// 多上限:T 必须是 Serializable 且 Comparable 的子类
public class MultiBound<T extends Serializable & Comparable<T>> {
private T data;
}
// 使用
NumberBox<Integer> intBox = new NumberBox<>();
intBox.setValue(10);
System.out.println(intBox.getDoubleValue()); // 10.0
NumberBox<String> strBox; // 编译报错:String 不是 Number 子类
2. 泛型方法的重载与重写
重载:泛型类型参数不同不算重载(类型擦除后相同)
java
运行
public class GenericOverload {
// 编译报错:类型擦除后都是 Object 参数,方法签名重复
// public void show(String str) {}
// public void show(T t) {}
}
重写:桥接方法保证重写正确性
java
运行
// 父类
public class Parent<T> {
public T getValue() {
return null;
}
}
// 子类
public class Child extends Parent<String> {
// 重写父类方法(泛型擦除后父类是 Object,子类是 String)
@Override
public String getValue() {
return "Java";
}
// 编译器自动生成桥接方法(保证重写规则)
public Object getValue() {
return getValue(); // 调用 String 版本的 getValue
}
}
3. 自定义泛型注解(扩展)
java
运行
// 定义泛型注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GenericAnnotation<T> {
Class<T> value();
}
// 使用
public class User {
@GenericAnnotation(String.class)
private String name;
@GenericAnnotation(Integer.class)
private Integer age;
}
五、泛型的常见坑点与避坑指南
1. 坑点 1:泛型类不能实例化类型参数
java
运行
public class Test<T> {
public Test() {
// 编译报错:类型擦除后 T 是 Object,无法实例化
// T t = new T();
// 正确方式:通过 Class 对象实例化
}
// 正确写法
public T createInstance(Class<T> clazz) throws InstantiationException, IllegalAccessException {
return clazz.newInstance();
}
}
2. 坑点 2:泛型异常不能捕获 / 抛出
java
运行
// 编译报错:泛型不能用于异常类型
// public class GenericException<T> extends Exception {}
// 编译报错:不能捕获泛型异常
// try { ... } catch (T e) { ... }
3. 坑点 3:泛型通配符的读写限制
? extends T:只读不写(能读 T 类型,无法确定具体子类,写入会报错);? super T:只写不读(能写 T 类型,读取只能是 Object);- 记忆口诀:PECS(Producer Extends, Consumer Super)
- 生产者(提供数据)用
extends(如读取集合元素); - 消费者(消费数据)用
super(如向集合写入元素)。
- 生产者(提供数据)用
4. 坑点 4:泛型类型不参与重载
如前文所述,类型擦除后泛型参数会被擦除,导致方法签名重复,无法重载。
六、泛型的典型应用场景
1. 集合框架(核心)
Java 集合全部基于泛型实现,如 ArrayList<T>、HashMap<K,V>、HashSet<T> 等,是泛型最核心的应用。
2. 自定义工具类
java
运行
// 泛型工具类:通用转换器
public class ConverterUtil {
// 将 List<S> 转换为 List<T>
public static <S, T> List<T> convert(List<S> source, Function<S, T> converter) {
List<T> target = new ArrayList<>();
for (S s : source) {
target.add(converter.apply(s));
}
return target;
}
}
// 使用
List<String> strList = Arrays.asList("1", "2", "3");
// 转换为 Integer 列表
List<Integer> intList = ConverterUtil.convert(strList, Integer::valueOf);
3. 通用返回结果(接口开发)
java
运行
// 泛型返回结果:适配所有接口返回类型
public class Result<T> {
private int code;
private String msg;
private T data;
// 静态工厂方法
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("success");
result.setData(data);
return result;
}
public static <T> Result<T> fail(String msg) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMsg(msg);
result.setData(null);
return result;
}
// getter/setter 省略
}
// 使用
Result<User> userResult = Result.success(new User("张三", 20));
Result<List<User>> listResult = Result.success(Arrays.asList(userResult.getData()));
七、常见面试题
- **泛型的作用?**编译期类型安全、避免强制类型转换、代码复用。
- **什么是类型擦除?**Java 泛型仅存在编译期,运行时泛型类型被擦除为上限类型(Object),JVM 无泛型信息。
- 泛型通配符?extends T 和?super T 的区别?
extends是上限,只读不写;super是下限,只写不读(PECS 原则)。 - **为什么不能创建泛型数组?**类型擦除后数组类型无法确定,运行时会破坏类型安全。
- **泛型方法和泛型类的区别?**泛型类的类型参数作用于整个类,泛型方法的类型参数仅作用于方法(可在非泛型类中定义)。
总结
- 核心本质:泛型是 Java 编译期特性,通过「类型参数化」实现类型安全和代码复用,运行时通过类型擦除变为原生类型;
- 核心用法:泛型类 / 接口、泛型方法、通配符(?/extends/super),遵循 PECS 原则;
- 核心避坑:类型擦除导致的运行时无泛型、不能实例化泛型参数、不能创建泛型数组。