Skip to content

什么是泛型

Java 泛型(Generics)是自 JDK 5.0 版本引入的一项关键特性,它将参数化类型的概念引入到 Java 编程中。泛型的本质是为了创建可操作多种数据类型的类、接口和方法,同时提供编译时期的类型安全检查。这项特性显著提升了代码的可读性、健壮性和复用性。

核心优势

引入泛型主要是为了解决两个核心问题:类型安全和代码重用。

  • 类型安全:在泛型出现之前,Java 的集合类(如 ArrayList)只能存储 Object 类型的对象。这意味着在从集合中取出对象时,必须进行强制类型转换,这不仅繁琐,而且容易在运行时引发 ClassCastException 异常。泛型在编译时期就对类型进行检查,从而避免了这类错误的发生。
  • 消除强制类型转换:通过在声明集合时指定其可存储的元素类型,可以省去大量的强制类型转换代码,使代码更加简洁、清晰。
  • 代码复用:泛型允许程序员编写通用的算法和数据结构,这些代码可以安全地应用于多种不同的数据类型,而无需为每种类型都编写重复的逻辑。

基本语法

泛型通过尖括号 <> 来声明类型参数,这些参数在类、接口或方法被使用时,会被具体的类型所替代。

1. 泛型类

泛型类是在实例化时接受类型参数的类。其定义语法如下:

class Box<T> {
    private T t;

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

    public T get() {
        return t;
    }
}

在这个例子中,T 是一个类型参数,可以在创建 Box 对象时指定为任何类类型。

Box<Integer> integerBox = new Box<>();
integerBox.set(10); // 正确
// integerBox.set("hello"); // 编译时错误

Box<String> stringBox = new Box<>();
stringBox.set("Hello World"); // 正确

2. 泛型接口

与泛型类类似,接口也可以通过泛型来定义。

interface Generator<T> {
    T next();
}

实现该接口的类,可以选择指定具体的类型,也可以继续使用泛型。

class RandomIntegerGenerator implements Generator<Integer> {
    private Random rand = new Random();

    @Override
    public Integer next() {
        return rand.nextInt(100);
    }
}

3. 泛型方法

泛型方法是在调用时才指明具体类型的方法,它可以在普通类中定义,也可以在泛型类中定义。 泛型方法的类型参数声明位于方法修饰符和返回类型之间。

public class Utils {
    public static <T> void printArray(T[] inputArray) {
        for (T element : inputArray) {
            System.out.printf("%s ", element);
        }
        System.out.println();
    }
}

调用泛型方法时,编译器通常会利用类型推断来确定 T 的具体类型。

Integer[] intArray = { 1, 2, 3, 4, 5 };
String[] stringArray = { "Hello", "World" };

Utils.printArray(intArray);
Utils.printArray(stringArray);

注意: 静态方法如果要使用泛型,必须被定义为泛型方法,因为它无法访问类上定义的泛型类型。

泛型通配符

通配符 ? 是 Java 泛型中一个强大而灵活的概念,用于表示未知的类型。

1. 无界通配符 <?>

无界通配符 <?> 表示可以是任何类型,它在逻辑上是所有 List<具体类型> 的父类。 当一个方法的参数类型是 List<?> 时,你不能向这个列表中添加任何元素(null 除外),因为编译器无法确定元素的具体类型是否匹配。但是,你可以安全地读取元素,因为你知道它们都是 Object 类型的。

public static void printList(List<?> list) {
    for (Object elem : list) {
        System.out.print(elem + " ");
    }
    System.out.println();
}

2. 上界通配符 <? extends T>

上界通配符 <? extends T> 表示参数化的类型必须是 TT 的子类。 这通常用于当你需要从一个数据结构中读取数据(作为生产者)的场景。

例如,一个接受 List<? extends Number> 的方法,可以处理 List<Integer>List<Double> 等,因为 IntegerDouble 都是 Number 的子类。你不能往这个列表中添加元素(null 除外),因为编译器无法确定列表的确切类型,但你可以安全地将元素读取为 Number 类型。

3. 下界通配符 <? super T>

下界通配符 <? super T> 表示参数化的类型必须是 TT 的父类。 这通常用于当你需要向一个数据结构中写入数据(作为消费者)的场景。

例如,一个接受 List<? super Integer> 的方法,可以处理 List<Integer>List<Number>List<Object>。你可以安全地向这个列表中添加 Integer 或其子类型的对象,因为它们都可以向上转型为 T。但是,当你从中读取数据时,只能保证它们是 Object 类型的。

核心机制:类型擦除

Java 泛型是通过一种被称为“类型擦除”(Type Erasure)的机制来实现的。 这意味着泛型信息只存在于编译阶段,在生成的字节码中,所有的泛型类型参数都会被替换为其边界类型(对于无界的类型参数,则替换为 Object)。

例如,Box<T> 在编译后会变成 Box,其内部的 T 类型会被替换为 Object。编译器会在必要的地方自动插入类型转换代码,以保证类型安全。

为什么需要类型擦除?

类型擦除主要是为了向后兼容。在 JDK 5.0 引入泛型时,需要确保新的泛型代码能够与旧的、非泛型的代码库兼容。

类型擦除的影响:

  • 无法获取运行时泛型类型:在运行时,ArrayList<String>ArrayList<Integer>getClass() 方法都将返回 ArrayList.class,因为泛型信息已被擦除。
  • 不能创建泛型数组:你不能直接创建像 new T[] 这样的泛型数组,因为数组的类型在运行时必须是具体的。
  • 不能实例化类型参数:不能创建类型参数的实例,例如 new T()
  • 静态上下文中不能使用类的类型参数:静态变量或方法不能引用其所在类的类型参数。