Java 泛型底层原理详解:什么是类型擦除?

一、泛型是什么?

Java 泛型是 JDK 5 引入的一种语法机制,主要作用是:

  1. 提供编译期类型检查;
  2. 减少强制类型转换;
  3. 提高代码复用性;
  4. 让代码语义更加清晰。

没有泛型之前,我们使用集合时通常是这样写的:

List list = new ArrayList();

list.add("hello");
list.add(123);

String s = (String) list.get(0);
String s2 = (String) list.get(1); // 运行时报 ClassCastException

这种写法的问题是:什么类型都能放进去,编译器无法提前检查;取出来时还需要手动强制类型转换,容易在运行时报错。

有了泛型之后:

List<String> list = new ArrayList<>();

list.add("hello");
// list.add(123); // 编译期直接报错

String s = list.get(0);

这样编译器就能在编译阶段帮我们检查类型错误。


二、泛型的底层原理是什么?

Java 泛型的底层原理是:

类型擦除。

也就是说,Java 泛型主要在编译期生效。编译器会根据泛型做类型检查,但是编译成 .class 字节码之后,大部分泛型类型信息会被擦除。

所以 Java 的泛型也被称为:

伪泛型。

因为运行时并不会真正存在 List<String>List<Integer> 这种不同类型。


三、什么是类型擦除?

类型擦除就是:

编译器在编译阶段会把泛型类型信息擦掉,把泛型类型替换成 Object 或者泛型的上界类型。

例如我们写的代码:

List<String> list = new ArrayList<>();
list.add("hello");

String s = list.get(0);

编译之后,大致可以理解成:

List list = new ArrayList();
list.add("hello");

String s = (String) list.get(0);

也就是说,编译器帮我们做了两件事:

  1. 编译期检查 List<String> 只能放 String
  2. 编译后擦除泛型信息,并在取值时自动插入强制类型转换。

四、List 和 List 运行时是同一个类型

看下面这个例子:

import java.util.ArrayList;
import java.util.List;

public class GenericTest {
    public static void main(String[] args) {
        List<String> list1 = new ArrayList<>();
        List<Integer> list2 = new ArrayList<>();

        System.out.println(list1.getClass());
        System.out.println(list2.getClass());
        System.out.println(list1.getClass() == list2.getClass());
    }
}

输出结果:

class java.util.ArrayList
class java.util.ArrayList
true

这说明:

List<String>List<Integer> 在运行时都是 ArrayList

运行时 JVM 并不知道这个集合的泛型参数到底是 String 还是 Integer

这就是类型擦除的体现。


五、泛型会被擦除成什么?

泛型擦除分两种情况:

  1. 没有上界,擦除成 Object
  2. 有上界,擦除成上界类型。

六、没有上界:擦除成 Object

比如:

public class Box<T> {

    private T value;

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

    public T get() {
        return value;
    }
}

这里的 T 没有指定上界。

编译后大致可以理解成:

public class Box {

    private Object value;

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

    public Object get() {
        return value;
    }
}

使用时:

Box<String> box = new Box<>();
box.set("hello");

String s = box.get();

编译器会自动帮我们插入强制类型转换,大致相当于:

Box box = new Box();
box.set("hello");

String s = (String) box.get();

所以:

如果泛型没有上界,类型擦除后会变成 Object


七、有上界:擦除成上界类型

例如:

public class NumberBox<T extends Number> {

    private T value;

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

    public double getDoubleValue() {
        return value.doubleValue();
    }
}

这里:

T extends Number

表示 T 必须是 Number 或者 Number 的子类,例如:

Integer
Long
Double
BigDecimal

编译后,T 会被擦除成它的上界 Number

public class NumberBox {

    private Number value;

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

    public double getDoubleValue() {
        return value.doubleValue();
    }
}

所以:

如果泛型有上界,类型擦除后会变成它的上界类型。


八、为什么泛型不能使用基本数据类型?

下面这种写法是错误的:

List<int> list = new ArrayList<>(); // 编译错误

正确写法是:

List<Integer> list = new ArrayList<>();

原因是:

Java 泛型经过类型擦除后,需要被替换成 Object 或者某个引用类型。但是 intlongdouble 这些基本类型不是对象,所以不能作为泛型参数。

因此泛型只能使用引用类型:

List<Integer> list1 = new ArrayList<>();
List<Long> list2 = new ArrayList<>();
List<String> list3 = new ArrayList<>();
List<User> list4 = new ArrayList<>();

九、为什么不能 new T?

下面这种代码也是错误的:

public class Factory<T> {

    public T create() {
        return new T(); // 编译错误
    }
}

原因是:

泛型在运行时已经被擦除了,JVM 不知道 T 到底是什么类型。

例如:

Factory<User> userFactory = new Factory<>();
Factory<Order> orderFactory = new Factory<>();

编译后泛型信息被擦掉,运行时 JVM 并不知道 TUser 还是 Order

所以不能直接:

new T();

如果确实要创建泛型对象,通常可以传入 Class<T>

public class Factory<T> {

    private final Class<T> clazz;

    public Factory(Class<T> clazz) {
        this.clazz = clazz;
    }

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

使用:

Factory<User> factory = new Factory<>(User.class);
User user = factory.create();

十、为什么不能创建泛型数组?

下面这种写法也不允许:

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

原因是:

数组在运行时会检查真实类型,而泛型在运行时会被擦除,两者机制冲突。

如果允许创建泛型数组,可能会出现类型不安全的问题。

例如:

List<String>[] arr = new List<String>[10];
Object[] objects = arr;

objects[0] = new ArrayList<Integer>();

String s = arr[0].get(0);

数组运行时只能知道它是 List[],但不知道它应该是 List<String>[]

所以 Java 禁止直接创建泛型数组。


十一、为什么 List 和 List 不能重载?

下面两个方法不能同时存在:

public void test(List<String> list) {
}

public void test(List<Integer> list) {
}

原因是类型擦除后,它们都会变成:

public void test(List list) {
}

方法签名完全一样,所以编译器会报错。

这也是类型擦除带来的限制。


十二、泛型的桥接方法

泛型擦除后,可能会影响 Java 的多态。为了保证多态正常,编译器有时会生成桥接方法。

看下面这个例子:

class Parent<T> {
    public T get() {
        return null;
    }
}

class Child extends Parent<String> {
    @Override
    public String get() {
        return "hello";
    }
}

泛型擦除后,父类大致变成:

class Parent {
    public Object get() {
        return null;
    }
}

子类是:

class Child extends Parent {
    public String get() {
        return "hello";
    }
}

为了保证父类引用调用子类方法时仍然符合多态,编译器会生成桥接方法,大致类似:

class Child extends Parent {

    public String get() {
        return "hello";
    }

    // 编译器生成的桥接方法
    public Object get() {
        return this.get();
    }
}

这个方法就是桥接方法。

它的作用是:

保证类型擦除后,泛型继承和多态仍然可以正常工作。


十三、泛型的优点

泛型的主要优点有:

  1. 编译期类型检查;
  2. 减少强制类型转换;
  3. 提高代码复用性;
  4. 让 API 语义更加清晰。

例如:

Map<String, User> userMap = new HashMap<>();

看到这个声明,我们马上就知道:

key 是 String 类型;
value 是 User 类型。

代码语义非常清晰。


十四、泛型的局限性

由于 Java 泛型是通过类型擦除实现的,所以它也有一些限制:

  1. 运行时无法直接获取 T 的真实类型;
  2. 不能直接 new T()
  3. 不能创建泛型数组;
  4. 不能使用基本类型作为泛型参数;
  5. List<String>List<Integer> 运行时是同一个类型;
  6. 不能通过 List<String>List<Integer> 作为方法参数来重载方法。

十五、面试标准回答

如果面试官问:

Java 泛型的底层原理是什么?

可以这样回答:

Java 泛型的底层原理是类型擦除。泛型主要在编译期生效,编译器会利用泛型做类型检查,比如 List<String> 只能放 String,不能放 Integer。但是编译成字节码之后,大部分泛型信息会被擦除,所以运行时 List<String>List<Integer> 本质上都是 List,它们的 Class 对象是一样的。

类型擦除时,如果泛型没有上界,比如 T,会被擦除成 Object;如果有上界,比如 T extends Number,会被擦除成 Number。同时,编译器会在取值处自动插入强制类型转换,比如 String s = list.get(0) 编译后类似于 String s = (String) list.get(0)

因为泛型被擦除了,所以 Java 不能直接 new T(),不能使用基本类型作为泛型参数,不能创建泛型数组,也不能用 List<String>List<Integer> 作为方法重载参数。为了保证泛型擦除后的多态正确,编译器有时还会生成桥接方法。

所以总结来说,Java 泛型是编译期的类型安全机制,底层通过类型擦除实现,运行时大部分泛型信息并不存在。


十六、总结

Java 泛型的核心可以总结为一句话:

泛型是编译期的类型安全机制,底层通过类型擦除实现。

再精简一点:

编译前:List<String>
编译后:List
取值时:编译器自动加类型强转

泛型带来的好处是类型安全和代码复用,但由于类型擦除,也带来了不能 new T()、不能使用基本类型、不能创建泛型数组等限制。

掌握泛型的关键,就是理解:

泛型主要存在于编译期,运行时大部分泛型信息已经被擦除了。

更多推荐