跳转至

相关文章

类型擦除&桥接方法 Java泛型原理篇: 类型擦除以及桥接方法 Java泛型的协变与逆变 你懂的逆变协变

泛型

You:

java 中的泛型擦除是什么意思


Java 中的泛型擦除(Generics Erasure)是指在编译时期,Java 编译器会擦除泛型信息,将泛型类型转换为原始类型。这是为了兼容 Java 早期版本的非泛型代码,以及为了保持与 Java 虚拟机(JVM)的向后兼容性。

泛型擦除的过程包括以下几个方面:

  1. 类型擦除:编译器会将泛型类型擦除为它们的原始类型。例如,List 在编译时会擦除为 List。

  2. 类型参数擦除:泛型类型中的类型参数信息也会被擦除。例如,List 和 List 在编译后都会被擦除为 List。

  3. 桥方法生成:当泛型类或接口中存在泛型方法时,编译器会生成桥方法来保持与原始类或接口的兼容性。这些桥方法会被添加到字节码中,以确保正确的方法调用。

由于泛型擦除的存在,导致在运行时无法获取泛型类型的具体信息。例如,在运行时无法知道 List 还是 List,因为它们在编译后都被擦除为 List。这就是 Java 中所谓的泛型类型信息丢失问题。泛型的类型信息在编译期间会检查避免一些类型错误

泛型擦除的优点是可以确保向后兼容性,并且可以减少在生成的字节码中泛型类型信息的冗余。然而,泛型擦除也导致了一些限制,如无法在运行时获取泛型类型信息,以及一些泛型类型转换的限制(泛型不能协变,编译期不会通过)。


You:

桥方法生成是什么意思


在Java中,泛型擦除会导致一个特殊的问题,即擦除后的泛型类型可能会丢失一些信息,尤其是在涉及到泛型方法和泛型子类型的情况下。为了保持 Java 的类型安全性,编译器会生成桥方法(Bridge Method)来处理这种情况。

桥方法的生成是为了保证类型安全性和向后兼容性,适应多态。当一个类或接口实现了一个泛型接口或继承了一个泛型类时,并且在子类或实现类中使用了泛型类型,编译器会生成桥方法来确保类型安全性。桥方法会强转类型然后调用子方法


逆变协变

协变(covariance)和逆变(contravariance)是面向对象编程中的两个重要概念,通常与泛型和子类型多态性相关联。它们描述了类型在子类型关系中的行为。

  1. 协变(Covariance)

    • 协变是指在子类型关系中,类型参数的子类型关系与其参数化类型之间的关系保持一致。
    • 在协变中,如果类型 S 是类型 T 的子类型(即 S <: T),那么类型 F<S> 是类型 F<T> 的子类型。
  2. 逆变(Contravariance)

    • 逆变是指在子类型关系中,类型参数的子类型关系与其参数化类型之间的关系相反。
    • 在逆变中,如果类型 S 是类型 T 的子类型(即 S <: T),那么类型 F<T> 是类型 F<S> 的子类型。

Java中的泛型是不变(invariance)的,但是在使用一门面向对象的语言中,我们难免会有需要集合也支持一些面向对象的特性的场景。Java的数组是协变的,泛型是不变的。但泛型可以通过extends关键字实现协变,通过super关键字实现逆变,分别应用于不同的场景。协变应用于消费场景,定义了上界。逆变应用于生产场景,定义了下界

关于Java的通配符如何使用, Effective Java, 3rd Edition 的作者将其总结为:PECS : stands for Producer-Extends, Consumer-Super. 结合上面代码分析是不是觉得很精辟。

Producer-Extends 只能调用读取方法,向外提供数据,无法调用修改方法 读是生产 Consumer-Super 一般只调用修改方法,消费从外面获取的数据,调用读取方法几乎没什么用,拿到的类型永远是Object 写是消费

interface BoxJ<T> {
      T getAnimal();
      void putAnimal(T a);
  }

//协变,可以接受BoxJ<Dog>类型的参数  BoxJ<Animal>也可以
 private Animal getOutAnimalFromBox(BoxJ<? extends Animal> box) {
       Animal animal = box.getAnimal();
      //无法调用该修改方法,因为当调用此方法时无法确定 ?究竟是一个什么类型,没办法传入 ?是一个特定类型 ?想象成无穷小
      // box.putAnimal(某个类型) 
       return animal;
  }

//逆变,可以接受BoxJ<Animal>类型的参数   BoxJ<Animal>也可以
 private void putAnimalInBox(BoxJ<? super Dog> box){
  //java func 可以接受子类对象于该类型的对象 所以可以接受Dog(优先去判定声明类型)
        box.putAnimal(new Dog());
        // 虽然可以调用读取方法,但返回的类型却是Object,因为我们只能确定 ?的最顶层基类是Object
        Object animal= box.getAnimal();
  }
List和List<?extend String>是不一样的即?是一个具体的类型且不支持协变逆变

Type obj = new _(); X可能为Type 也可能是子类构造器 func(obj) java方法调用优先看是否有与对象声明类型类型相同的同名方法再看有没有obj声明类型的父类型的同名方法去调用 即func的调用类似遵循协变但是优先匹配与对象相同的声明类型相同的方法

Java 泛型中的通配符详解:? extends T? super T

本文整理自腾讯云开发者社区文章,主要讲解 Java 泛型通配符的使用方式与适用场景。

为什么需要通配符?

在 Java 中,泛型是不具备协变性的。即使 AppleFruit 的子类,Plate<Apple> 也不是 Plate<Fruit> 的子类。为了表达泛型类型间的灵活关系,引入了通配符:

  • ? extends T:上界通配符
  • ? super T:下界通配符

? extends T 上界通配符

表示该泛型类型是 T 或其子类。

Plate<? extends Fruit> plate = new Plate<Apple>();

这种通配符主要用于读取数据。不能写入数据(除了 null),因为编译器无法确认具体类型。

适用场景:

  • 泛型对象是数据的“生产者”(Producer)
  • 只能安全读取,不保证写入安全

? super T 下界通配符

表示该泛型类型是 T 或其父类。

Plate<? super Fruit> plate = new Plate<Object>();

这种通配符主要用于写入数据(只能addT以及其子类;但是List<? super Dog> list = new ArrayList(Arrays.asList("a", "b", "c"));是可以的)。读取时由于具体类型不确定,只能当作 Object 处理。

适用场景:

  • 泛型对象是数据的“消费者”(Consumer)
  • 可安全写入,但读取时类型信息丢失

PECS 原则

Java 泛型使用中的经典原则:

  • Producer Extends:需要从中读取数据时,使用 ? extends T
  • Consumer Super:需要向其中写入数据时,使用 ? super T

注意事项

  • 上界通配符 ? extends T可读不可写
  • 下界通配符 ? super T可写但读类型不确定

总结

通配符 表示范围 适合操作 可写入 可读取
? extends T T 及其子类 读取
? super T T 及其父类 写入 仅作 Object

通过通配符的灵活运用,可以使 Java 泛型在保持类型安全的同时,增强代码的通用性与可复用性。

回到页面顶部