Skip to content

泛型基础

概述

泛型(Generics)是 JDK 5 引入的语法特性,允许在定义接口方法时使用类型参数(如 TE),在使用时再指定具体类型。泛型的主要目的是:在编译期做类型检查,避免把错误留到运行时;同时减少强制类型转换,让代码更安全、更易读。

通俗理解:泛型就像「带占位符的模板」——先写「这里将来会是一种类型 T」,等真正用的时候再告诉编译器「T 是 String」或「T 是 Integer」,编译器会按你指定的类型做检查。

前置建议

已掌握 类与对象接口,理解多态与向上转型。泛型在 集合Lambda/Stream 中大量使用,建议在学集合前先打好泛型基础。


为什么需要泛型

在没有泛型时,若要让一个类能「装任意类型」,往往使用 Object 引用,取用时再强转,容易在运行时出现 ClassCastException

java
// 泛型出现前:用 Object 表示「任意类型」
List list = new ArrayList();
list.add("hello");
list.add(100);
String s = (String) list.get(1);  // 运行时 ClassCastException!

使用泛型后,类型在编译期就确定,错误在编译阶段就能被发现:

java
List<String> list = new ArrayList<>();
list.add("hello");
list.add(100);           // 编译错误:不能把 Integer 放进 List<String>
String s = list.get(0);  // 无需强转,取出的就是 String

泛型带来的好处

  • 类型安全:编译期检查类型,减少运行时类型转换错误。
  • 消除强转:取出的元素已是目标类型,无需手写 (String) 等。
  • 代码复用:同一套类/方法可适用于多种类型,而不必为每种类型写重复代码。

泛型类

在类名后加 <类型参数> 即可定义泛型类。类型参数常用单字母:T(Type)、E(Element)、K/V(Key/Value)、N(Number)等。

基本语法

java
// 类名后声明类型参数 T,在类体内可把 T 当作一种类型使用
public class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

使用示例

java
// 指定 T 为 String
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();   // 无需强转

// 指定 T 为 Integer
Box<Integer> intBox = new Box<>();
intBox.set(100);
Integer n = intBox.get();

// 若不指定泛型(原始类型),会失去类型检查,不推荐
Box rawBox = new Box();
rawBox.set("hello");
// 取用时需强转,且容易出错

注意

使用泛型类时尽量始终带上类型实参(如 Box<String>),避免使用原始类型(raw type)Box,否则会退回「Object + 强转」的旧写法,失去泛型的类型安全。

多个类型参数

类可以声明多个类型参数,用逗号分隔,例如表示键值对的类:

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; }
}

// 使用
Pair<String, Integer> pair = new Pair<>("age", 25);
String k = pair.getKey();    // "age"
Integer v = pair.getValue(); // 25

泛型接口

接口也可以带类型参数,定义与泛型类类似;实现类在实现接口时指定具体类型,或保持为泛型类。

定义泛型接口

java
// 可比较的「盒子」,比较逻辑由实现类决定,但比较的是同类型 T
public interface ComparableBox<T> {
    int compareTo(T other);
}

实现方式一:实现时指定具体类型

java
public class StringBox implements ComparableBox<String> {
    private String value;

    public StringBox(String value) {
        this.value = value;
    }

    @Override
    public int compareTo(String other) {
        return value.compareTo(other);
    }
}

实现方式二:实现类仍是泛型类

java
public class Box<T> implements ComparableBox<T> {
    private T value;
    // 假设 T 本身实现了 Comparable<T>,这里仅作演示
    private java.util.Comparator<T> comparator;

    public Box(T value, java.util.Comparator<T> comparator) {
        this.value = value;
        this.comparator = comparator;
    }

    @Override
    public int compareTo(T other) {
        return comparator.compare(value, other);
    }
}

标准库中的 ListSetMap 等都是泛型接口,使用时会写成 List<String>Map<Integer, String> 等。


泛型方法

泛型方法是在方法上声明类型参数,而不是在类上。格式为:在修饰符与返回类型之间<类型参数>,方法参数或返回类型中即可使用该类型参数。

基本语法

java
// 在 static 与返回类型 void 之间声明类型参数 T
public static <T> void printArray(T[] array) {
    for (T elem : array) {
        System.out.println(elem);
    }
}

// 调用时编译器根据实参推断 T
printArray(new String[]{"a", "b"});  // T 推断为 String
printArray(new Integer[]{1, 2, 3});  // T 推断为 Integer

带返回值的泛型方法

java
// 从列表中取第一个元素,列表元素类型为 T,返回类型也是 T
public static <T> T getFirst(List<T> list) {
    return list.isEmpty() ? null : list.get(0);
}

String first = getFirst(Arrays.asList("a", "b", "c"));  // "a"
Integer num = getFirst(Arrays.asList(1, 2, 3));          // 1

泛型方法与泛型类的区别

  • 泛型类:类型参数写在类名后,整个类的实例都与该类型绑定(如 Box<String>)。
  • 泛型方法:类型参数写在方法上,仅该方法使用该类型;可以是静态方法,且每次调用可以推断出不同的类型。
java
public class Utils {
    // 静态泛型方法:类不需要是泛型类
    public static <T> boolean isEmpty(List<T> list) {
        return list == null || list.isEmpty();
    }
}

提示

当方法逻辑「与具体类型无关、只与结构有关」时,用泛型方法即可,不必把整个类都写成泛型类。


类型擦除

Java 的泛型在编译后并不会保留在字节码里,而是通过 类型擦除(type erasure)被擦掉:编译器把类型参数替换成其上界(未指定上界时即为 Object),并在需要时插入强制转换。因此,运行时 JVM 看不到泛型信息,只能看到「原始类型」。

擦除后的效果

java
// 源码
public class Box<T> {
    private T value;
    public T get() { return value; }
}

// 擦除后(逻辑上等价,实际字节码中 T 变为 Object)
// class Box {
//     private Object value;
//     public Object get() { return value; }
// }

因此:

  • Box<String>Box<Integer> 在运行时都只是 Box无法在运行时区分
  • 不能写出 if (obj instanceof Box<String>) 这样的判断(可写 obj instanceof Box,但拿不到 String)。
  • 不能写 new T()new T[],因为运行时没有 T 的信息。

类型擦除带来的限制

限制说明
不能使用基本类型作为类型实参只能写 List<Integer>,不能写 List<int>,需用包装类
不能 new T() / new T[]运行时不知道 T 是谁,无法实例化
静态成员不能引用类型参数静态属于类,而 T 属于实例,故 static T field 非法
不能创建参数化类型的数组new List<String>[10] 不合法,可写 new List<?>[10] 或用集合

示例:不合法写法

java
public class Example<T> {
    // 错误:静态成员不能使用类型参数 T
    // private static T staticField;

    public void bad() {
        // 错误:不能 new T()
        // T obj = new T();

        // 错误:不能 new T[]
        // T[] array = new T[10];
    }
}

// 错误:不能创建 List<String> 的数组
// List<String>[] arr = new List<String>[10];

版本说明

若需在运行时保留类型信息,可配合反射在运行时读取泛型签名(如 ParameterizedType),但日常业务代码中更多是接受类型擦除、在编译期利用泛型做类型检查即可。


注意事项

注意

  • 重写带泛型的方法时,子类方法上的类型参数或签名要与父类/接口一致,避免变成重载或编译错误。
  • 泛型只存在于编译期,运行时无法根据「是不是 List<String>」做分支,只能根据「是不是 List」判断。

提示

  • 类型参数命名建议统一:T 表示一般类型,E 表示集合元素,K/V 表示键/值,N 表示数字,便于团队阅读。
  • 在学完 通配符 后,可以更灵活地声明方法参数(如 List<? extends Number>),既保证类型安全又提高 API 的适用性。

相关链接

基于 VitePress 构建