Z
ZHANK

泛型基础

理解泛型类、泛型方法、类型擦除机制

泛型基础

泛型(Generics)是 Java 5 引入的重大特性。它让你在编译期就能发现类型错误,而不是等到运行时才抛出 ClassCastException

学完本章你将: 理解为什么需要泛型、定义泛型类和泛型方法、认识类型擦除机制。


为什么需要泛型

在没有泛型的时代,集合里可以放任意类型的对象,取出来必须强制转型。一旦放错类型,编译不报错,运行直接崩溃。

java
// ❌ Java 5 之前的写法 —— 类型不安全
List list = new ArrayList();
list.add("hello");
list.add(123);               // 完全合法!编译不报错
String s = (String) list.get(0); // 被迫强转
String s2 = (String) list.get(1); // 💥 运行时 ClassCastException!

泛型解决了这个问题: 在声明集合时就约定好"只能放 String",编译器帮你把关。

java
// ✅ 使用泛型 —— 编译期检查
List<String> strList = new ArrayList<>();
strList.add("hello");         // ✅ 正确类型
// strList.add(123);          // ❌ 编译就报错,根本跑不起来
String s = strList.get(0);    // 无需强转,类型已确定

💡 核心思想: 泛型把类型检查从"运行时"提前到"编译时"。越早发现错误,代价越低。


泛型类

泛型类就是在类名后加 <T>T 是个类型参数(占位符),真正使用时才指定具体类型。

可以把 Box<T> 想象成"一个能装任意类型东西的盒子"。定义时还不知道装什么——T 就是那个未知类型。使用时 Box<String> 就锁定了——这个盒子只能装字符串。

java
// 定义泛型类:<T> 是类型参数,可以叫任何名字(如 T、E、K、V)
public class Box<T> {
    private T value;           // T 类型的成员变量

    public void set(T value) { this.value = value; }  // 参数是 T
    public T get() { return value; }                  // 返回值是 T
    public boolean isEmpty() { return value == null; }
}
java
// 使用时指定具体类型
Box<String> strBox = new Box<>();  // T 被替换为 String
strBox.set("Hello");               // 只能传 String
String val = strBox.get();         // 取出就是 String,不用强转

Box<Integer> intBox = new Box<>(); // T 被替换为 Integer
intBox.set(42);

约定俗成的类型参数名:

字母含义常见场景
`T`Type通用类型
`E`Element集合元素
`K`KeyMap 的键
`V`ValueMap 的值
`R`Return返回值类型

泛型方法

泛型方法可以独立于泛型类存在。即使一个普通类,也可以有泛型方法。方法名前的 <T> 声明了"这个方法使用类型参数 T",然后参数和返回值就能用 T。

和泛型类的区别: 泛型类的 T 作用域是整个类,泛型方法的 T 作用域只是这一个方法。两者互不影响。

java
public class Utils {

    // <T> 声明类型参数  T 作为返回类型  List<T> 使用类型参数
    public static <T> T getFirst(List<T> list) {
        if (list.isEmpty()) return null;
        return list.get(0);
    }

    // 交换数组中两个位置 —— 能用于任意类型数组
    public static <T> void swap(T[] arr, int i, int j) {
        T temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
java
// 调用泛型方法时,编译器自动推断 T 的类型
Integer[] nums = {1, 2, 3};
Utils.swap(nums, 0, 2);       // 自动推断 T = Integer
System.out.println(Arrays.toString(nums)); // [3, 2, 1]

String[] words = {"A", "B", "C"};
Utils.swap(words, 0, 1);      // 自动推断 T = String

⚠️ 注意: 调用时通常不需要显式写 <Integer>,编译器能根据你传入的参数类型自动推断。只有推断失败时才需要手动指定。


类型擦除

这是泛型最容易被误解的地方。泛型只存在于编译期。 编译完成后,所有 <T> 会被擦除,替换为 Object(或上界类型)。

java
// 写的代码
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

// 编译后变成(伪代码)
List strList = new ArrayList();    // <String> 被擦除了
List intList = new ArrayList();    // <Integer> 也被擦除了

所以运行时两者的 Class 完全一样:

java
System.out.println(strList.getClass() == intList.getClass()); // true!

类型擦除带来的限制:

java
// ❌ 不能 new 泛型数组 —— 擦除后 JVM 不知道 T 是什么类型
// T[] arr = new T[10];              // 编译错误
// List<String>[] arr = new List<String>[10]; // 编译错误

// ❌ 不能直接用 instanceof 检查泛型
// if (obj instanceof List<String>) {} // 编译错误(擦除后只剩 List)

// ❌ 不能重载泛型方法 —— 擦除后签名相同
// void method(List<String> list) {}
// void method(List<Integer> list) {}  // 编译错误!擦除后签名一致

💡 为什么 Java 要擦除? 为了向后兼容。Java 5 引入泛型时,必须保证旧的 Java 1.4 字节码仍能在新 JVM 上运行。如果泛型在运行时保留,所有旧代码都得重写。