Skip to content

泛型的核心作用

为什么需要泛型?

写代码时,你一定被这个问题折磨过:

java
// 没有泛型:往 List 里塞什么,全靠约定
List list = new ArrayList();
list.add("hello");
list.add(123); // 鬼知道这里会放什么

// 取出来?强制类型转换
String s = (String) list.get(1); // 运行时报错:ClassCastException

编译时不报错,运行时才崩。这种「记错类型」的代价,往往在生产环境才能发现。

泛型的出现,就是为了把这个问题提前到编译期

泛型做了什么

用泛型重写上面的代码:

java
List<String> list = new ArrayList<>();
list.add("hello");
list.add(123); // 编译错误! IDE 直接告诉你类型不匹配

String s = list.get(0); // 不需要强制转换,类型已经确定

编译器像是一个严格的检查员:在你写代码的时候就拦住错误,而不是等到用户用的时候才崩溃。

三个核心作用

1. 类型安全

编译器负责类型检查,把 ClassCastException 从运行时搬到编译时。

java
// 错误在编译期暴露
List<String> list = new ArrayList<>();
list.add(123); // 编译错误:incompatible types

2. 消除强制类型转换

没有泛型时,每次取数据都要转型:

java
// 没有泛型
Map map = new HashMap();
map.put("name", "张三");
String name = (String) map.get("name"); // 需要转型

// 有泛型
Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String name = map.get("name"); // 直接用,类型已经确定

3. 代码复用

泛型让同一套逻辑可以作用于不同类型:

java
// 不是写三个方法,而是写一个通用的
public <T> void print(T item) {
    System.out.println(item);
}

print("hello");  // T = String
print(123);      // T = Integer
print(true);     // T = Boolean

泛型的使用场景

泛型类

java
// 创建一个可以装任何东西的盒子,但取出来时类型是明确的
public class Box<T> {
    private T content;

    public void put(T item) {
        this.content = item;
    }

    public T get() {
        return content;
    }
}

Box<String> stringBox = new Box<>();
stringBox.put("物品");
// String item = stringBox.get(); // 类型安全

泛型接口

java
// 定义一个可以生成任意类型结果的接口
public interface Generator<T> {
    T generate();
}

// 实现时指定具体类型
public class StringGenerator implements Generator<String> {
    @Override
    public String generate() {
        return "generated string";
    }
}

泛型方法

java
// 方法级别的泛型,不影响整个类
public class Utils {
    // 交换任意类型数组的两个元素
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

String[] names = {"Alice", "Bob"};
Utils.swap(names, 0, 1); // 编译时确定 T = String

泛型限定

限制泛型可以接受的类型范围:

java
// 只接受 Number 及其子类
public class NumberBox<T extends Number> {
    private T value;

    public void printDouble() {
        // 确定 T 是 Number,可以调用 doubleValue()
        System.out.println(value.doubleValue());
    }
}

NumberBox<Integer> intBox = new NumberBox<>(); // OK
NumberBox<String> strBox = new NumberBox<>();  // 编译错误

泛型的局限性

泛型不是银弹,它有一些本质限制:

基本类型不能作为类型参数

java
List<int> list;     // 编译错误
List<Integer> list; // 正确,需要使用包装类

原因:泛型是为了类型安全,而基本类型和对象在 JVM 里有本质区别。

泛型类型不能实例化

java
T item = new T(); // 编译错误

因为 T 在运行时会被擦除,new T() 在运行时根本不知道该 new 什么。

泛型数组不能直接创建

java
T[] array = new T[10]; // 编译错误

泛型类型不能用于静态上下文

java
public class Container<T> {
    private static T instance; // 编译错误

    public static T getInstance() { // 编译错误
        return instance;
    }
}

静态成员属于类,不属于实例,而泛型是实例级别的概念,两者天然冲突。

泛型与多态

一个常见的困惑:

java
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // 编译错误!

List<Dog> 不是 List<Animal> 的子类。这叫类型系统的不变性

为什么?假设可以赋值:

java
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // 假设可以

animals.add(new Cat()); // 往 Dog 列表里塞了 Cat!
Dog dog = dogs.get(0);  // 运行时崩溃

这是为了类型安全付出的代价。具体怎么突破这个限制,后面讲通配符时会详细介绍。

总结

泛型干了一件什么事?

让编译器帮你记住类型,把运行时的错误提前到编译时。

记住这个核心,理解泛型的各种规则就会顺畅很多:

特性原因
类型参数不能是基本类型泛型只作用于对象,运行时需要类型信息
不能 new T()类型擦除后 T 在运行时不存在
List<Dog> 不是 List<Animal>不变性保证类型安全
不能用 static T静态成员属于类,泛型属于实例

基于 VitePress 构建