Skip to content

泛型

为什么需要泛型?

在没有泛型的 Java 1.4 时代,集合操作依赖 Object 类型存储和强制转型:

java
List list = new ArrayList();
list.add("hello");
list.add(123);          // 编译通过,但逻辑上可能是个 bug
String s = (String) list.get(0);  // 必须强转,类型不安全

这带来三个问题:

  1. 类型不安全list.add(123) 不会报错,运行时才会抛出 ClassCastException
  2. 代码冗余:每次从集合取出元素都要写 (String) 强转
  3. 可读性差:看代码无法立即知道集合里存的是什么类型

泛型(JDK 1.5 引入)在编译期就解决了这些问题:

java
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123);      // 编译直接报错
String s = list.get(0); // 无需强转,编译器保证类型安全

泛型的核心价值:将运行时的类型错误提前到编译期发现


泛型的基本概念

类型参数化

泛型的本质是类型参数化——把类型当作参数传递,让同一段代码适配多种数据类型。

java
// 没有泛型——每种类型写一个类
class IntegerBox { private Integer value; ... }
class StringBox  { private String value;  ... }

// 有泛型——一个类通吃所有类型
class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

类型参数命名约定

字母含义典型场景
EElement集合元素(List<E>
KKey键(Map<K,V>
VValue值(Map<K,V>
TType通用类型
S, U第二、第三类型参数多参数场景
?通配符未知类型

泛型类

在类名后声明类型参数,类内部即可使用:

java
public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey()   { return key; }
    public V getValue() { return value; }

    public void setKey(K key)      { this.key = key; }
    public void setValue(V value)  { this.value = value; }
}

// 使用
Pair<String, Integer> pair = new Pair<>("age", 25);
String key = pair.getKey();   // 无需强转
Integer value = pair.getValue();

泛型类中的静态成员

静态成员不能引用类的类型参数,因为类型参数在实例化时才确定,而静态成员属于类本身:

java
public class Container<T> {
    // private static T value;     // 编译错误!
    // 静态方法如果想用泛型,必须声明自己的类型参数
    public static <U> U identity(U value) {
        return value;
    }
}

泛型接口

java
public interface Repository<T, ID> {
    T findById(ID id);
    void save(T entity);
    List<T> findAll();
}

// 实现时指定具体类型
public class UserRepository implements Repository<User, Long> {
    @Override
    public User findById(Long id) { /* ... */ return null; }

    @Override
    public void save(User entity) { /* ... */ }

    @Override
    public List<User> findAll() { /* ... */ return null; }
}

// 实现类也可以保留泛型
public class GenericRepository<T, ID> implements Repository<T, ID> {
    // 实现方法...
}

泛型方法

方法可以独立于类声明自己的类型参数:

java
public class Utils {
    // 泛型方法:类型参数声明在返回值前
    public static <T> T getMiddle(T... items) {
        return items[items.length / 2];
    }

    // 类型参数带边界
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
}

// 调用
String middle = Utils.<String>getMiddle("a", "b", "c"); // 显式指定
String middle2 = Utils.getMiddle("a", "b", "c");        // 类型推断

类型擦除

擦除原理

Java 泛型是编译期概念,运行时泛型信息会被"擦除"。这是为了向后兼容——JVM 不需要理解泛型,老代码可以直接运行在新 JVM 上。

java
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

// 运行时,两者都是普通 ArrayList
System.out.println(strList.getClass() == intList.getClass()); // true
System.out.println(strList.getClass());  // class java.util.ArrayList

擦除规则

泛型形式擦除结果
List<T>(无边界)ListT 擦除为 Object
List<T extends Number>ListT 擦除为 Number
List<?>List
java
// 编译后等价于:
public class Box {
    private Object value;  // T 被擦除为 Object
    public void set(Object value) { this.value = value; }
    public Object get() { return value; }
}

桥接方法(Bridge Method)

类型擦除后,编译器会生成桥接方法保证多态性:

java
class Node<T> {
    private T data;
    public void setData(T data) { this.data = data; }
}

class MyNode extends Node<Integer> {
    @Override
    public void setData(Integer data) { /* ... */ }
    // 编译器自动生成桥接方法:
    // public void setData(Object data) { setData((Integer) data); }
}

这是反编译后能看到 volatile / bridge 标记的方法的原因。


泛型边界

上界通配符 ? extends T

限制类型必须是 T 或其子类。只能读,不能写(除了 null):

java
// 能接受 List<Integer>、List<Double>、List<Number> 等
public static double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();  // 可以读,当作 Number 处理
    }
    // numbers.add(10);  // 编译错误!不能写
    return total;
}

List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
System.out.println(sum(ints));    // 6.0
System.out.println(sum(doubles)); // 7.5

下界通配符 ? super T

限制类型必须是 T 或其父类。可以写,读出来是 Object

java
public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 5; i++) {
        list.add(i);  // 可以写 Integer
    }
    // Integer num = list.get(0);  // 编译错误!读出来是 Object
}

List<Number> numbers = new ArrayList<>();
addNumbers(numbers);  // OK,Number 是 Integer 的父类
List<Object> objects = new ArrayList<>();
addNumbers(objects);  // OK,Object 是 Integer 的父类

PECS 原则

Producer Extends, Consumer Super

  • 如果只需要读取数据(生产者),用 ? extends T
  • 如果只需要写入数据(消费者),用 ? super T
  • 如果既读又写,直接用具体类型 <T>
java
// Collections.copy 的签名完美体现了 PECS:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    // src 是生产者(读取),用 extends
    // dest 是消费者(写入),用 super
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));
    }
}

无界通配符 <?>

表示未知类型,等同于 <? extends Object>

java
// 当方法只用到 Object 的方法时,用 <?> 比 <T> 语义更清晰
public static void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

泛型的限制

不能使用基本类型

泛型参数必须是引用类型,不能是 intdouble 等基本类型:

java
// List<int> list = new ArrayList<>();    // 编译错误
List<Integer> list = new ArrayList<>();     // 正确,用包装类

原因:类型擦除后 T 被替换为 ObjectObject 不能存储基本类型值。

不能创建泛型数组

java
// T[] array = new T[10];         // 编译错误
// List<String>[] arr = new List<String>[10];  // 编译错误

原因:数组在运行时保留元素类型信息,而泛型在运行时被擦除,两者冲突。如果允许,会破坏类型安全:

java
// 假设允许创建泛型数组:
// List<String>[] strArr = new List<String>[10];
// Object[] objArr = strArr;          // 数组协变
// objArr[0] = new ArrayList<Integer>(); // 能通过数组检查
// String s = strArr[0].get(0);       // ClassCastException!

变通方案:使用 ArrayList 代替数组,或创建通配符数组后强转:

java
List<String>[] arr = (List<String>[]) new ArrayList[10]; // 警告,但可行

不能 instanceof 泛型类型

java
// if (list instanceof List<String>) {}  // 编译错误
if (list instanceof List) {}              // 只能检查原始类型

原因:运行时泛型信息已被擦除,无法区分 List<String>List<Integer>

不能实例化类型参数

java
public <T> T create() {
    // return new T();  // 编译错误
    return null;
}

原因:类型擦除后 T 变成 Object,并且编译器不知道 T 是否有无参构造器。需要使用反射或 Supplier

java
public <T> T create(Class<T> clazz) throws Exception {
    return clazz.getDeclaredConstructor().newInstance();
}

public <T> T create(Supplier<T> supplier) {
    return supplier.get();
}

不能抛出泛型异常

java
// class MyException<T> extends Exception {}  // 编译错误
// catch (T e) {}                               // 编译错误

不能在静态上下文中使用类型参数

java
class Foo<T> {
    // static T value;  // 编译错误
    // static void method(T t) {}  // 编译错误
}

泛型与继承关系

List<String> 不是 List<Object> 的子类型。即使 StringObject 的子类,泛型集合也不具备协变性:

java
List<String> strList = new ArrayList<>();
// List<Object> objList = strList;  // 编译错误!

这是为了类型安全。如果允许,就会出现:

java
// 假设允许
// List<Object> objList = strList;
// objList.add(123);  // 把一个 Integer 塞进了 String 列表
// String s = strList.get(0);  // 运行时 ClassCastException

常见面试题

Q1: Java 泛型是如何实现的?什么是类型擦除?

Java 泛型通过类型擦除实现,泛型信息只存在于编译期。编译后,所有类型参数被替换为边界类型(无边界则为 Object),并插入必要的强制转型代码。这是为了向后兼容老版本 JVM。

Q2: List<?>List<Object>List 有什么区别?

声明含义可以 add 吗?
List原始类型,无泛型检查可以 add 任何对象,但编译警告
List<Object>明确存放 Object可以 add 任何对象
List<?>未知类型,只读只能 add null

List<?>List<Object> 不是同一个类型,List<?> 是只读的。

Q3: PECS 原则是什么?

Producer Extends, Consumer Super。如果从集合读取数据,使用 ? extends T;如果向集合写入数据,使用 ? super T;如果既读又写,直接用 <T>

Q4: 为什么不能创建泛型数组?

数组在运行时保留元素类型(协变),泛型在运行时被擦除。如果允许泛型数组,会导致数组的类型检查被绕过,进而引发运行时的 ClassCastException

Q5: 桥接方法是什么?为什么需要它?

类型擦除后,子类重写父类泛型方法时,方法签名可能不匹配。编译器自动生成一个桥接方法(类型擦除后的签名),在桥接方法内部调用子类实际方法,保证多态性正常工作。

Q6: 泛型方法中的 <T> 放在什么位置?

放在返回值类型之前。例如 public static <T> T getValue(T t)

Q7: 为什么不能 catch (T e)throw new T()

JVM 需要知道异常的具体类型来匹配 catch 块。类型擦除后 T 变成 Object,JVM 无法判断实际类型。同时泛型类不能继承 Throwable