从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是如何设计的呢,敬请期待...

相关推荐
墨痕诉清风几秒前
java漏洞集合工具(Struts2、Fastjson、Weblogic(xml)、Shiro、Log4j、Jboss、SpringCloud)
xml·java·struts·安全·web安全·spring cloud·log4j
程序员阿鹏3 分钟前
SpringBoot自动装配原理
java·开发语言·spring boot·后端·spring·tomcat·maven
Andy工程师3 分钟前
一个接口可以有多个实现类
java
老华带你飞10 分钟前
工会管理|基于springboot 工会管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
自在极意功。10 分钟前
MyBatis配置文件详解:environments、transactionManager与dataSource全面解析
java·数据库·tomcat·mybatis
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ15 分钟前
配置springdoc swagger开关
java
Echo flower18 分钟前
Spring Boot WebFlux 实现流式数据传输与断点续传
java·spring boot·后端
没有bug.的程序员24 分钟前
微服务中的数据一致性困局
java·jvm·微服务·架构·wpf·电商
鸽鸽程序猿29 分钟前
【Redis】Java客户端使用Redis
java·redis·github
悦悦子a啊29 分钟前
使用 Java 集合类中的 LinkedList 模拟栈以此判断字符串是否是回文
java·开发语言