什么是泛型?它的核心原理是什么?
泛型(Generics)是 Java 的一种参数化类型 机制。简单来说,它允许我们在定义类、接口或方法时,用一个占位符(比如 <T>)来代替具体的类型,等到真正使用的时候再指定具体的数据类型。
核心原理:类型擦除(Type Erasure)
Java 的泛型是一种"伪泛型"。泛型只在编译阶段有效,编译器会通过泛型进行严格的类型检查。一旦编译通过,进入 JVM 运行时,所有的泛型信息都会被"擦除"。
- 无限制的泛型
<T>会被擦除为Object。 - 有限制的泛型
<T extends Number>会被擦除为Number。
泛型带来的三大好处:
- 类型安全 :将
ClassCastException(类型转换异常)的报错从"运行时"提前到了"编译期"。 - 消除强转:从集合中获取元素时,不需要再手动进行繁琐的强制类型转换。
- 代码复用:一套逻辑可以适配多种数据类型,避免了为每种类型重复编写代码。
常见类型参数命名约定
| 参数 | 含义 | 示例 |
|---|---|---|
T |
Type(通用类型) | Box<T> |
E |
Element(集合元素) | List<E> |
K |
Key(键) | Map<K, V> |
V |
Value(值) | Map<K, V> |
N |
Number(数值类型) | List<N extends Number> |
R |
Return(返回值) | <R> R method() |
️ 泛型的基础使用与语法
在 Java 中,泛型主要有三种使用方式:泛型类、泛型接口和泛型方法。
1. 泛型类
泛型类是在实例化对象的时候,明确具体的数据类型。
// 定义一个泛型类,T 代表 Type
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 使用泛型类
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello Generics");
String str = stringBox.getContent(); // 不需要强转,直接获取 String 类型
2. 泛型接口
泛型接口通常在实现类中确定具体类型,或者实现类也保持泛型。
// 定义泛型接口
public interface Generator<T> {
T next();
}
// 实现类确定泛型类型
public class FruitGenerator implements Generator<String> {
@Override
public String next() {
return "Apple";
}
}
3. 泛型方法
泛型方法非常灵活,它可以在普通类中定义,泛型类型在调用方法时动态确定。
public class ArrayUtils {
// 泛型方法:交换数组中的两个元素
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
企业级实战案例
在企业开发中,泛型被广泛应用于统一接口返回、工具类封装等场景。
案例一:统一 API 接口返回结果(最常用)
在前后端分离的开发中,我们需要一个统一的返回格式(包含状态码、提示信息、业务数据)。利用泛型,我们可以让 data 字段适配任意类型的业务数据(如 User、List<Order> 等)。
public class Result<T> {
private int code; // 状态码
private String msg; // 提示信息
private T data; // 泛型数据,可以是任意对象
// 私有构造,强制通过静态工厂方法创建
private Result(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
// 成功返回的静态工厂方法
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data);
}
// 失败返回的静态工厂方法
public static <T> Result<T> fail(String msg) {
return new Result<>(500, msg, null);
}
// 省略 getter 和 setter
}
// 业务代码中直接使用
public Result<User> getUserInfo() {
User user = new User("张三", 25);
return Result.success(user); // 自动适配 User 类型
}
案例二:结合枚举的通用工具类
在实际业务中,我们经常需要获取枚举的描述信息。通过泛型加接口约束,可以写出一个通用的枚举工具类。
// 1. 定义一个带有描述的枚举基础接口
public interface BaseEnum {
String getDesc();
}
// 2. 具体的业务枚举实现该接口
public enum UserRole implements BaseEnum {
ADMIN("管理员"),
USER("普通用户");
private final String desc;
UserRole(String desc) { this.desc = desc; }
@Override
public String getDesc() { return desc; }
}
// 3. 泛型工具类:约束传入的泛型 T 必须实现 BaseEnum 接口
public class EnumUtil {
public static <T extends BaseEnum> String getDesc(T enumObj) {
return enumObj == null ? "" : enumObj.getDesc();
}
}
// 使用
String desc = EnumUtil.getDesc(UserRole.ADMIN); // 输出:管理员
进阶:通配符与 PECS 原则
在使用泛型集合时,经常会遇到 ?、extends 和 super。记住 PECS 原则(Producer Extends, Consumer Super)就能轻松应对:
<? extends T>(上界通配符) :适用于只读场景(Producer)。它表示可以接受 T 或 T 的子类。因为不知道具体是哪个子类,所以不能往里存(除了 null),但可以安全地取出来当作 T 使用。<? super T>(下界通配符) :适用于只写场景(Consumer)。它表示可以接受 T 或 T 的父类。因为父类引用可以指向子类对象,所以可以安全地存入 T 及其子类,但取出来时只能当作 Object。
️ 泛型的常见限制(避坑指南)
- 不能使用基本类型 :泛型的类型参数必须是引用类型。例如,不能用
List<int>,必须用包装类List<Integer>。 - 不能创建泛型数组 :例如
T[] arr = new T[10];是非法的,因为泛型在运行时会被擦除,JVM 无法确定数组的具体类型。 - 静态方法不能使用类的泛型 :静态变量和静态方法属于类,在类加载时就已初始化,而类的泛型是在实例化对象时才确定的。如果静态方法需要泛型,必须将其定义为独立的泛型方法(如案例一中的
public static <T> Result<T> success)。 - 不能捕获泛型异常 :泛型类不能直接或间接继承
Throwable。