从0到1学习泛型

在学习 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<>();

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

"此外,泛型还为更复杂的类型关系(如协变、逆变)奠定了基础,这在函数式编程和 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 并不知道 listList<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 实例,而非你期望的 StringUser,这违背了泛型的语义,因此编译器直接禁止该写法。 替代方案:传入 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> 变成 BoxT getValue()Object getValue()
  • IntegerBox 中的 Integer getValue()Integer getValue()

此时,IntegerBoxgetValue() 并没有重写 父类的 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)

    AB 的子类型,且 List<A> 也是 List<B> 的子类型,则 List 是协变的。
    Java 中通过 ? extends 实现

    ini 复制代码
    List<String> strs = Arrays.asList("a", "b");
    List<? extends Object> objs = strs; // 协变(只读安全)
    Object o = objs.get(0); // 可读
    // objs.add("x");       //  编译错误:不能写
  • 逆变(Contravariant)

    AB 的子类型,但 List<B> 能当作 List<A> 使用,则是逆变。
    Java 中通过 ? super 实现

    ini 复制代码
    List<Object> objs = new ArrayList<>();
    List<? super String> strs = objs; //逆变(只写安全)
    strs.add("hello"); // 可写
    // String s = strs.get(0); // 不安全,只能返回 Object
  • 不变(Invariant)

    默认情况下,List<String>List<Object> 没有子类型关系

    ini 复制代码
    List<String> strs = new ArrayList<>();
    List<Object> objs = strs; //  编译错误!Java 泛型默认不变

协变用于生产数据(读) ,逆变用于消费数据(写) ,不变保证读写安全


写在最后:

我是一个刚接触编程的大学生,主要学习方向是Java后端,在意识到自己基础并不扎实,于是有了这篇文章,也是我的第一个脚印,虽然里面大部分内容都是我询问ai后得到的,但是我也花了大量时间整理,希望能帮助到你,以后的我看见这篇文章会是什么感受呢。 初衷:

1.深入基础,并可以复习

2.可以帮助到需要的人

3.记录学习的过程

路漫漫其修远兮,对于泛型Kotlin是如何设计的呢,敬请期待...

相关推荐
萤丰信息3 小时前
从超级大脑到智能毛细血管:四大技术重构智慧园区生态版图
java·人工智能·科技·重构·架构·智慧园区
帅得不敢出门3 小时前
Android监听第三方播放获取音乐信息及包名
android·java
qq_266348733 小时前
系统白名单接口添加自定义验证(模仿oauth2.0),防安全扫描不通过
java·安全
努力努力再努力wz3 小时前
【C++进阶系列】:万字详解特殊类以及设计模式
java·linux·运维·开发语言·数据结构·c++·设计模式
青云交4 小时前
Java 大视界 -- Java 大数据在智慧交通自动驾驶仿真与测试数据处理中的应用
java·大数据·自动驾驶·数据存储·算法优化·智慧交通·测试数据处理
reasonsummer4 小时前
【办公类-115-05】20250920职称资料上传04——PDF和PDF合并PDF、图片和PDF合并PDF(十三五PDF+十四五图片)
java·python·pdf
Mcband4 小时前
Apache Commons IO:文件流处理利器,让Java IO操作更简单
java·开发语言·apache
缺点内向4 小时前
Java:将 Word 文档转换为密码保护的 PDF 文件
java·pdf·word
骑士雄师4 小时前
Java 泛型中级面试题及答案
java·开发语言·面试