Java 泛型底层原理详解
Java 泛型底层原理详解:什么是类型擦除?
一、泛型是什么?
Java 泛型是 JDK 5 引入的一种语法机制,主要作用是:
- 提供编译期类型检查;
- 减少强制类型转换;
- 提高代码复用性;
- 让代码语义更加清晰。
没有泛型之前,我们使用集合时通常是这样写的:
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);
也就是说,编译器帮我们做了两件事:
- 编译期检查
List<String>只能放String; - 编译后擦除泛型信息,并在取值时自动插入强制类型转换。
四、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。
这就是类型擦除的体现。
五、泛型会被擦除成什么?
泛型擦除分两种情况:
- 没有上界,擦除成
Object; - 有上界,擦除成上界类型。
六、没有上界:擦除成 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或者某个引用类型。但是int、long、double这些基本类型不是对象,所以不能作为泛型参数。
因此泛型只能使用引用类型:
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 并不知道 T 是 User 还是 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();
}
}
这个方法就是桥接方法。
它的作用是:
保证类型擦除后,泛型继承和多态仍然可以正常工作。
十三、泛型的优点
泛型的主要优点有:
- 编译期类型检查;
- 减少强制类型转换;
- 提高代码复用性;
- 让 API 语义更加清晰。
例如:
Map<String, User> userMap = new HashMap<>();
看到这个声明,我们马上就知道:
key 是 String 类型;
value 是 User 类型。
代码语义非常清晰。
十四、泛型的局限性
由于 Java 泛型是通过类型擦除实现的,所以它也有一些限制:
- 运行时无法直接获取
T的真实类型; - 不能直接
new T(); - 不能创建泛型数组;
- 不能使用基本类型作为泛型参数;
List<String>和List<Integer>运行时是同一个类型;- 不能通过
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()、不能使用基本类型、不能创建泛型数组等限制。
掌握泛型的关键,就是理解:
泛型主要存在于编译期,运行时大部分泛型信息已经被擦除了。
更多推荐

所有评论(0)