在学习 Kotlin 泛型时,我发现对 Java 泛型的理解仍有盲区。本文围绕三个核心问题展开:泛型是什么?类型擦除如何工作?桥接方法为何存在 续篇:Kotlin中的泛型
一.泛型是什么,为什么要有泛型?
泛型(Generics)是编程语言中的一种特性,它允许在定义类、接口、方法时使用类型参数(type parameters),从而让代码可以适用于多种数据类型,同时在编译期保证类型安全。
在泛型出现之前(比如早期 Java 1.4 及以前),集合类(如 ArrayList
)只能存储 Object
类型,使用时需要手动强转,容易出错:
scss
// 没有泛型的代码(不安全)
List list = new ArrayList();
list.add("Hello");
list.add(123); // 编译器不会报错!
String s = (String) list.get(1); //你在使用的时候并不知道它是什么类型的,所有容易出错
就算类型是正确的也把必须强转,给编码带来极大的不便利, 所以,泛型应运而生,泛型最常见的使用场景就是各种集合,Map,List,Set等;
自动类型转换 → 无需强转
ini
List<String> list = new ArrayList<>();
list.add("泛型真好用");
String msg = list.get(0); //直接赋值,无需 (String) 强转
显而易见,代码的可读性与简易性大大提高,泛型就像一个约定,约定好用某一个类型,明确我需要一个怎样的集合;此外泛型还能显著提高代码的复用性
你可以编写一个通用的工具类,适用于多种类型:
typescript
class Box<T> {
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<>();
需为每种类型写一个 StringBox
、IntegerBox
......一套代码,多种用途 ,在很多工具类中都能看见它的身影,比如通用的"结果封装"类(类似 Optional
或 Result
)...由此可见泛型的功能多么强大,类型安全、无需强转、代码复用、接口清晰。
"此外,泛型还为更复杂的类型关系(如协变、逆变)奠定了基础,这在函数式编程和 API 设计中尤为重要。"
类型擦除
类型擦除 是指:
Java 编译器在编译泛型代码时,会将泛型类型参数(如
<T>
、<String>
)全部"擦除",替换为其上界(通常是Object
),并在必要处插入类型转换(cast)代码。最终生成的字节码中不包含泛型类型信息。
举个例子:
ini
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);
编译后,字节码等价于:
ini
List list = new ArrayList(); // 泛型信息被擦除
list.add("Hello");
String s = (String) list.get(0); // 编译器自动插入强转
也就是说,运行时 JVM 并不知道 list
是 List<String>
,它只知道这是一个 List
。
为什么要有类型擦除这个功能
设想一下,泛型是在Java1.5才出现的,那么之前的代码都没有泛型这个东西的,那么以前所有含有泛型的代码都要改,为了适配旧代码和新代码,类型擦除就诞生了,泛型是在 Java 1.5 引入的,而此前的代码(如 Java 1.4)大量使用原始类型(raw types)。为了保证新代码能与旧库无缝协作,Java 选择在编译期擦除泛型信息,使得生成的字节码与旧版本兼容。
类型擦除带来的问题
1. 不能创建泛型数组
ini
T[] arr = new T[10];
因为类型擦除后运行时不知道 T
是什么类型,JVM 无法创建正确类型的数组。
替代方案:
css
T[] arr = (T[]) new Object[10]; // 不安全,但有时可用(需 @SuppressWarnings)
2. 不能使用 instanceof
检查具体泛型类型
javascript
if (obj instanceof List<String>) { ... } // 编译错误!
因为运行时 List<String>
和 List<Integer>
都是 List
,无法区分。
只能检查原始类型:
java
if (obj instanceof List) { ... }
3. 不能直接实例化类型参数
csharp
public T create() {
return new T(); //
}
即使 new T()
被编译成 new Object()
,返回的也是 Object
实例,而非你期望的 String
或 User
,这违背了泛型的语义,因此编译器直接禁止该写法。 替代方案:传入 Class<T>
对象
java
public T create(Class<T> clazz) throws Exception {
return clazz.newInstance();
}
4. 泛型类的静态成员不能使用类型参数
csharp
public class Box<T> {
private static T value; //错误
}
因为静态成员属于类本身,而 T
是实例级别的(且会被擦除)。
桥接方法
桥接方法(Bridge Method) 是 Java 编译器为了解决泛型类型擦除带来的多态问题 而自动生成的合成方法(synthetic method)。它是 Java 泛型实现中一个关键但对开发者透明的机制。
一、为什么需要桥接方法?
背景:类型擦除 + 方法重写 = 出现问题
在 Java 语言层面,方法签名不包含返回类型,因此
Integer getValue()
被视为重写了Number getValue()
。但在 JVM 字节码中,方法签名包含返回类型 ,因此Object getValue()
和Integer getValue()
是两个不同的方法。类型擦除后,父类方法变为Object getValue()
,而子类方法仍是Integer getValue()
,JVM 无法识别这是重写------于是编译器生成桥接方法来'桥接'这一 gap。
考虑以下代码:
scala
class Parent {
public Number getValue() {
return 100;
}
}
class Child extends Parent {
@Override
public Integer getValue() { // 注意:返回类型是 Integer(Number 的子类)
return 42;
}
}
这在 Java 中是合法的(协变返回类型,covariant return type)。
但如果我们用泛型来写:
scala
class Box<T> {
public T getValue() {
return null;
}
}
class IntegerBox extends Box<Integer> {
@Override
public Integer getValue() { // 看似合理
return 42;
}
}
问题来了:
由于类型擦除,编译后:
Box<T>
变成Box
,T getValue()
→Object getValue()
IntegerBox
中的Integer getValue()
→Integer getValue()
此时,IntegerBox
的 getValue()
并没有重写 父类的 Object getValue()
,因为方法签名不同(返回类型不同,但 Java 方法重写要求签名完全一致,包括返回类型在字节码层面)。
这会导致多态失效
ini
Box<Integer> box = new IntegerBox();
Integer v = box.getValue(); // 期望调用子类方法,但 JVM 找不到匹配的重写方法!
二、桥接方法如何解决这个问题?
Java 编译器会自动在子类中生成一个"桥接方法" ,它:
- 方法签名与父类擦除后的方法一致(
Object getValue()
) - 内部调用子类的实际方法(
Integer getValue()
) - 返回时自动转型
编译后,IntegerBox
实际变成:
scala
class IntegerBox extends Box<Integer> {
// 你写的实际方法
public Integer getValue() {
return 42;
}
// 编译器自动生成的桥接方法(synthetic)
public Object getValue() {
return getValue(); // 调用上面的 Integer getValue()
}
}
这样:
- 父类引用调用
getValue()
时,JVM 找到的是Object getValue()
(桥接方法) - 桥接方法内部调用真正的
Integer getValue()
- 返回的
Integer
会被自动向上转型为Object
(符合 JVM 要求)
补充:协变/不变/逆变
泛型类型是否能随其类型参数的子类型关系而"传递",决定了它是协变、逆变还是不变。
-
协变(Covariant) :
若
A
是B
的子类型,且List<A>
也是List<B>
的子类型,则List
是协变的。
Java 中通过? extends
实现:iniList<String> strs = Arrays.asList("a", "b"); List<? extends Object> objs = strs; // 协变(只读安全) Object o = objs.get(0); // 可读 // objs.add("x"); // 编译错误:不能写
-
逆变(Contravariant) :
若
A
是B
的子类型,但List<B>
能当作List<A>
使用,则是逆变。
Java 中通过? super
实现:iniList<Object> objs = new ArrayList<>(); List<? super String> strs = objs; //逆变(只写安全) strs.add("hello"); // 可写 // String s = strs.get(0); // 不安全,只能返回 Object
-
不变(Invariant) :
默认情况下,
List<String>
和List<Object>
没有子类型关系:iniList<String> strs = new ArrayList<>(); List<Object> objs = strs; // 编译错误!Java 泛型默认不变
协变用于生产数据(读) ,逆变用于消费数据(写) ,不变保证读写安全。
写在最后:
我是一个刚接触编程的大学生,主要学习方向是Java后端,在意识到自己基础并不扎实,于是有了这篇文章,也是我的第一个脚印,虽然里面大部分内容都是我询问ai后得到的,但是我也花了大量时间整理,希望能帮助到你,以后的我看见这篇文章会是什么感受呢。 初衷:
1.深入基础,并可以复习
2.可以帮助到需要的人
3.记录学习的过程
路漫漫其修远兮,对于泛型Kotlin是如何设计的呢,敬请期待...