在学习 Kotlin 泛型时,我发现对 Java 泛型的理解仍有盲区。本文围绕三个核心问题展开:泛型是什么?类型擦除如何工作?桥接方法为何存在 续篇:Kotlin中的泛型

一.泛型是什么,为什么要有泛型?

在泛型出现之前(比如早期 Java 1.4 及以前),集合类(如 ArrayList)只能存储 Object 类型,使用时需要手动强转,容易出错:

// 没有泛型的代码(不安全)
List list = new ArrayList();
list.add("Hello");
list.add(123); // 编译器不会报错!

String s = (String) list.get(1); //你在使用的时候并不知道它是什么类型的,所有容易出错

就算类型是正确的也把必须强转,给编码带来极大的不便利, 所以,泛型应运而生,泛型最常见的使用场景就是各种集合,Map,List,Set等;

自动类型转换 → 无需强转

List<String> list = new ArrayList<>();
list.add("泛型真好用");
String msg = list.get(0); //直接赋值,无需 (String) 强转

显而易见,代码的可读性与简易性大大提高,泛型就像一个约定,约定好用某一个类型,明确我需要一个怎样的集合;此外泛型还能显著提高代码的复用性

你可以编写一个通用的工具类,适用于多种类型:

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……一套代码,多种用途,在很多工具类中都能看见它的身影,比如通用的“结果封装”类(类似 Optional 或 Result)...由此可见泛型的功能多么强大,类型安全、无需强转、代码复用、接口清晰。

类型擦除

类型擦除 是指:

举个例子:

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);     // 编译器自动插入强转

也就是说,运行时 JVM 并不知道 listList<String>,它只知道这是一个 List

为什么要有类型擦除这个功能

设想一下,泛型是在Java1.5才出现的,那么之前的代码都没有泛型这个东西的,那么以前所有含有泛型的代码都要改,为了适配旧代码和新代码,类型擦除就诞生了,泛型是在 Java 1.5 引入的,而此前的代码(如 Java 1.4)大量使用原始类型(raw types)。为了保证新代码能与旧库无缝协作,Java 选择在编译期擦除泛型信息,使得生成的字节码与旧版本兼容。

类型擦除带来的问题

1. 不能创建泛型数组

T[] arr = new T[10];

因为类型擦除后运行时不知道 T 是什么类型,JVM 无法创建正确类型的数组。

替代方案:

T[] arr = (T[]) new Object[10]; // 不安全,但有时可用(需 @SuppressWarnings

2. 不能使用 instanceof 检查具体泛型类型

if (obj instanceof List<String>) { ... } //  编译错误!

因为运行时 List<String>List<Integer> 都是 List,无法区分。

只能检查原始类型:

if (obj instanceof List) { ... } 

3. 不能直接实例化类型参数

public T create() {
    return new T(); // 
}

即使 new T() 被编译成 new Object(),返回的也是 Object 实例,而非你期望的 StringUser,这违背了泛型的语义,因此编译器直接禁止该写法。 替代方案:传入 Class<T> 对象

public T create(Class<T> clazz) throws Exception {
    return clazz.newInstance();
}

4. 泛型类的静态成员不能使用类型参数

public class Box<T> {
    private static T value; //错误
}

因为静态成员属于类本身,而 T 是实例级别的(且会被擦除)。

桥接方法

一、为什么需要桥接方法?

背景:类型擦除 + 方法重写 = 出现问题

考虑以下代码:

class Parent {
    public Number getValue() {
        return 100;
    }
}

class Child extends Parent {
    @Override
    public Integer getValue() {  // 注意:返回类型是 Integer(Number 的子类)
        return 42;
    }
}

这在 Java 中是合法的(协变返回类型,covariant return type)。

但如果我们用泛型来写:

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 方法重写要求签名完全一致,包括返回类型在字节码层面)。

这会导致多态失效

Box<Integer> box = new IntegerBox();
Integer v = box.getValue(); // 期望调用子类方法,但 JVM 找不到匹配的重写方法!

二、桥接方法如何解决这个问题?

Java 编译器会自动在子类中生成一个“桥接方法” ,它:

  • 方法签名与父类擦除后的方法一致(Object getValue()
  • 内部调用子类的实际方法(Integer getValue()
  • 返回时自动转型

编译后,IntegerBox 实际变成:

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 实现

    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 实现

    List<Object> objs = new ArrayList<>();
    List<? super String> strs = objs; //逆变(只写安全)
    strs.add("hello"); // 可写
    // String s = strs.get(0); // 不安全,只能返回 Object
    
  • 不变(Invariant)
    默认情况下,List<String>List<Object> 没有子类型关系

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

写在最后:

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

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

2.可以帮助到需要的人

3.记录学习的过程

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

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]