从0到1学习泛型

Java教程 2025-10-14

在学习 Kotlin 泛型时,我发现对 Java 泛型的理解仍有盲区。本文围绕三个核心问题展开:泛型是什么?类型擦除如何工作?桥接方法为何存在 续篇:Kotlin中的泛型

一.泛型是什么,为什么要有泛型?

在泛型出现之前(比如早期 Java 1.4 及以前),集合类(如 ArrayList)只能存储 Object 类型,使用时需要手动强转,容易出错:

// 没有泛型的代码(不安全)
List list = new ArrayList();
list.add("Hello");
list.add(123); // 编译器不会报错!

String s = (String) list.get(1); //你在使用的时候并不知道它是什么类型的,所有容易出错

就算类型是正确的也把必须强转,给编码带来极大的不便利, 所以,泛型应运而生,泛型最常见的使用场景就是各种集合,Map,List,Set等;

自动类型转换 → 无需强转

List list = new ArrayList<>();
list.add("泛型真好用");
String msg = list.get(0); //直接赋值,无需 (String) 强转

显而易见,代码的可读性与简易性大大提高,泛型就像一个约定,约定好用某一个类型,明确我需要一个怎样的集合;此外泛型还能显著提高代码的复用性

你可以编写一个通用的工具类,适用于多种类型:

class Box {
    private T value;
    public T get() { return value; }
    public void set(T value) { this.value = value; }
}

// 使用
Box<String> stringBox = new Box<>();
Box<Integer> intBox = new Box<>();

需为每种类型写一个 StringBoxIntegerBox……一套代码,多种用途,在很多工具类中都能看见它的身影,比如通用的“结果封装”类(类似 Optional 或 Result)...由此可见泛型的功能多么强大,类型安全、无需强转、代码复用、接口清晰。

类型擦除

类型擦除 是指:

举个例子:

List list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);

编译后,字节码等价于

List list = new ArrayList();          // 泛型信息被擦除
list.add("Hello");
String s = (String) list.get(0);     // 编译器自动插入强转

也就是说,运行时 JVM 并不知道 listList,它只知道这是一个 List

为什么要有类型擦除这个功能

设想一下,泛型是在Java1.5才出现的,那么之前的代码都没有泛型这个东西的,那么以前所有含有泛型的代码都要改,为了适配旧代码和新代码,类型擦除就诞生了,泛型是在 Java 1.5 引入的,而此前的代码(如 Java 1.4)大量使用原始类型(raw types)。为了保证新代码能与旧库无缝协作,Java 选择在编译期擦除泛型信息,使得生成的字节码与旧版本兼容。

类型擦除带来的问题

1. 不能创建泛型数组

T[] arr = new T[10];

因为类型擦除后运行时不知道 T 是什么类型,JVM 无法创建正确类型的数组。

替代方案:

T[] arr = (T[]) new Object[10]; // 不安全,但有时可用(需 @SuppressWarnings

2. 不能使用 instanceof 检查具体泛型类型

if (obj instanceof List<String>) { ... } //  编译错误!

因为运行时 ListList 都是 List,无法区分。

只能检查原始类型:

if (obj instanceof List) { ... } 

3. 不能直接实例化类型参数

public T create() {
    return new T(); // 
}

即使 new T() 被编译成 new Object(),返回的也是 Object 实例,而非你期望的 StringUser,这违背了泛型的语义,因此编译器直接禁止该写法。 替代方案:传入 Class 对象

public T create(Class clazz) throws Exception {
    return clazz.newInstance();
}

4. 泛型类的静态成员不能使用类型参数

public class Box<T> {
    private static T value; //错误
}

因为静态成员属于类本身,而 T 是实例级别的(且会被擦除)。

桥接方法

一、为什么需要桥接方法?

背景:类型擦除 + 方法重写 = 出现问题

考虑以下代码:

class Parent {
    public Number getValue() {
        return 100;
    }
}

class Child extends Parent {
    @Override
    public Integer getValue() {  // 注意:返回类型是 Integer(Number 的子类)
        return 42;
    }
}

这在 Java 中是合法的(协变返回类型,covariant return type)。

但如果我们用泛型来写:

class Box {
    public T getValue() {
        return null;
    }
}

class IntegerBox extends Box {
    @Override
    public Integer getValue() {  // 看似合理
        return 42;
    }
}

问题来了:

由于类型擦除,编译后:

  • Box 变成 BoxT getValue() → Object getValue()
  • IntegerBox 中的 Integer getValue() → Integer getValue()

此时,IntegerBoxgetValue() 并没有重写父类的 Object getValue(),因为方法签名不同(返回类型不同,但 Java 方法重写要求签名完全一致,包括返回类型在字节码层面)。

这会导致多态失效

Box box = new IntegerBox();
Integer v = box.getValue(); // 期望调用子类方法,但 JVM 找不到匹配的重写方法!

二、桥接方法如何解决这个问题?

Java 编译器会自动在子类中生成一个“桥接方法” ,它:

  • 方法签名与父类擦除后的方法一致(Object getValue()
  • 内部调用子类的实际方法(Integer getValue()
  • 返回时自动转型

编译后,IntegerBox 实际变成:

class IntegerBox extends Box {
    // 你写的实际方法
    public Integer getValue() {
        return 42;
    }

    // 编译器自动生成的桥接方法(synthetic)
    public Object getValue() {
        return getValue(); // 调用上面的 Integer getValue()
    }
}

这样:

  • 父类引用调用 getValue() 时,JVM 找到的是 Object getValue()(桥接方法)
  • 桥接方法内部调用真正的 Integer getValue()
  • 返回的 Integer 会被自动向上转型为 Object(符合 JVM 要求)

补充:协变/不变/逆变

泛型类型是否能随其类型参数的子类型关系而“传递”,决定了它是协变、逆变还是不变。