Java学习之什么是泛型的擦除

上一章讲了泛型的基本使用方式,但是在上一章中提到了擦除这一概念。

我们都知道Java一开始并没有泛型这一语言特性,泛型这一特性是在JavaSE5出现的,也就是说,在该版本以前,所有的库都是以原有的语言特性进行设计,如果我们在java代码中使用了新的语言特性,那么编译出来的代码是不被低版本的jre进行兼容的。换句话说,如果泛型以新语言特性的形式出现,那么所有需要使用到泛型的类库都需要在新版本的编译器上进行重新编译,而原来的老的类型会因为低版本jre不兼容这些新的语言特性而无法使用这些类库,那么我们这时候必须将这些老的类库进行重新编译。但是原有的老的类库是机器庞大的,我们不可能为了泛型这一特性而抛弃旧有类库(过于庞大),因此在实现代码迁移时,我们为了向旧有类库兼容,其每个类库应当不需要判断其他类库是否使用了泛型。因此在编译时,我们通过擦除来实现伪泛型。也就是说在写代码时,我们使用了泛型,但这种泛型并非真正的泛型,是一种语法糖,编译器会在编译器将泛型的信息擦掉,换成兼容原有类库的代码。
因此,擦除是在不破坏原有类库的情况下,将泛型融入java

编译时是怎么擦除泛型的

我们现在举一个泛型的例子
这是一个我们常规定义的一个泛型类,它只有简单的getset方法,用于对泛型类型的持有

public class SimpleGeneric<T> {
    
    
    private T t;

    public T getT() {
    
    
        return t;
    }

    public void setT(T t) {
    
    
        this.t = t;
    }

    public static void main(String[] args) {
    
    
        SimpleGeneric<String> gs = new SimpleGeneric<>();
        gs.setT("hello");
        String string = gs.getT();
    }
}

编译后

public class SimpleGeneric {
    
    
    private Object t;

    public Object getT() {
    
    
        return t;
    }

    public void setT(Object t) {
    
    
        this.t = t;
    }

    public static void main(String[] args) {
    
    
        SimpleGeneric gs = new SimpleGeneric();
        gs.setT("hello");
        String string = (String)gs.getT();
    }
}

比对了一下编译前后的代码我们会发现其中所有T的部分都会被置换为了Obejct。
同时,由于我们传入的类型参数是String,所以在取值时,编译器为我们动态添加了(String)的强制转换


我们再看一个例子
下面例子中,是对于同一个泛型类但是传入不同类型参数的比较。

public class SimpleGeneric<T> {
    
    
    private T t;

    public T getT() {
    
    
        return t;
    }

    public void setT(T t) {
    
    
        this.t = t;
    }

    public static void main(String[] args) {
    
    
        SimpleGeneric<String> gs1 = new SimpleGeneric<>();
        SimpleGeneric<String> gs2 = new SimpleGeneric<>();
        System.out.println(gs1.getClass() == gs2.getClass());//true
    }
}

比较结果显示,不论它们传入的是什么类型参数,容器类还是同一个容器类。这是对前面擦除的例子进行了一个验证

由于类型出现了参数,因此我们会发现,在泛型类中,我们只能通过T来调用一些Object拥有的方法,我们无法通过泛型类型去获取任何有关类型参数的信息。
我们在使用泛型时完全可以将泛型类中的泛型参数当作Object使用



边界

我们前面提到我们在泛型中只能使用Obejct的方法,因为泛型的类型参数会被擦除成为Object,但是这样一来,在一些很多我需要调用一些特殊方法的情况下不就不能使用了吗。那么这样依赖,所谓的泛型根本没有任何的存在必要,完全可以用一个Obejct来替换掉。其实泛型已经为我们考虑好了一切,既然泛型是为泛化而存在的,那它必然会存在一些泛化的特性。因此为了在泛型中调用特殊的方法,泛型引入了边界的概念。

边界使得我们可以在用于泛型的参数类型上设置限制条件,使得我们可以在泛型中根据边界类型来表用方法。因为一旦拥有了边界,编译器会在擦除时擦除到我们的第一个边界类型


<T extends XXX>

根据下面的例子我们发现在FruitContainer类中声明了<T extends Fruit>,这表示我们传入的参数类型必须继承自Fruit或者Fruit本身,也就是说,FruitContainer<T extends Fruit>中不关持有的是什么类型,它都必然是Fruit的子类或者Fruit本身,因此,这个类也必然可以安全的向上转型为Fruit,也就是说,我们完全可以通过T来调用属于Fruit的方法

public class BorderGenericDemo1 {
    
    
    public static void main(String[] args) {
    
    
        FruitContainer<Fruit> container = new FruitContainer<>();
        container.setFruit(new Fruit());//我是水果
        Fruit fruit = container.getFruit();

        container.setFruit(new Apple());//我是苹果
        Apple apple = (Apple) container.getFruit();
    }
}

class Fruit{
    
    
    public void f(){
    
    
        System.out.println("我是水果");
    }
}

class Apple extends Fruit{
    
    
    @Override
    public void f(){
    
    
        System.out.println("我是苹果");
    }
}

class FruitContainer<T extends Fruit>{
    
    
    private T t;

    public void setFruit(T t){
    
    
        this.t = t;
        t.f();
    }

    public T getFruit(){
    
    
        return t;
    }
}

编译后

class FruitContainer{
    
    
    private Fruit t;

    public void setFruit(Fruit t){
    
    
        this.t = t;
        t.f();
    }

    public Fruit getFruit(){
    
    
        return t;
    }
}

通过上面的编译前后的对比,我们可以这么认为,擦除就会将类型参数在编译时擦除为对应的边界



通配符与上下界

<? extends T>和<? super T>是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。
  • <? extends T>:是指 “上界通配符(Upper Bounds Wildcards)”
  • <? super T>:是指 “下界通配符(Lower Bounds Wildcards)”

为什么要用通配符和边界?–泛型不是协变的

开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收 List 作为形式参数,那么如果尝试将一个 List 的对象作为实际参数传进去,却发现无法通过编译。

虽然从直觉上来说,Object 是 String 的父类,这种类型转换应该是合理的。但是实际上这会产生隐含的类型转换问题,因此编译器直接就禁止这样的行为。

比如我们有Fruit类,和它的派生类Apple

class Fruit {
    
    }
class Apple extends Fruit {
    
    }

然后有一个最简单的容器:Plate类,盘子里可以放一个泛型的”东西”

我们可以对这个东西做最简单的“放”和“取”的动作:set( )和get( )方法。

class Plate<T>{
    
    
    private T item;
    public Plate(T t){
    
    item=t;}
    public void set(T t){
    
    item=t;}
    public T get(){
    
    return item;}
}

现定义一个“水果盘”,逻辑上水果盘当然可以装苹果。

Plate<Fruit> p=new Plate<Apple>(new Apple());

但实际上Java编译器不允许这个操作。会报错,“装苹果的盘子”无法转换成“装水果的盘子”。

error: incompatible types: Plate<Apple> cannot be converted to Plate<Fruit>

实际上,编译器认定的逻辑是这样的:
苹果 IS-A 水果
装苹果的盘子 NOT-IS-A 装水果的盘子

所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系。

所以我们不可以把Plate的引用传递给Plate。

泛型不是协变的

在 Java 语言中,数组是协变的,也就是说,如果 Integer 扩展了 Object,那么不仅 Integer 是 Object,而且 Integer[] 也是 Object[],在要求 Object[] 的地方完全可以传递或者赋予 Integer[]。

Integer [] numbers = new Integer[]{
    
    1,2,3};
Object [] objects = numbers;
String [] strings = (String[]) objects;

从前面的代码来看,Integer[] 直接向上转型成了 Object[],同时在第三行,我们将Object []转换成了String []。虽然我们知道Object []的真实类型是Integer[],是不可能转换成String [],但是编译器允许这样做的原因是编译器不知道objects中真正的类型是什么,它只知道这是Object数组的应用,里面应该都是Object,而我们将Object向下转型成为String这么一件事看起来也是没有问题的。

事实上,由于数组存在RTTI,也就是所谓的运行时类型检查,所以虽然这种向下转型是不安全的,但是完全可以在运行时发现问题并抛出异常,所以编译器允许这样的向下转型。但是注意,虽然编译器允许Object向下转型成String,但是Integer直接转型成为String是不被编译器允许的。

您也许认为这一原理同样适用于泛型类型 —— List< Number> 是 List< Integer> 的超类型,那么可以在需要 List< Number> 的地方传递 List< Integer>。不幸的是,情况并非如此。为啥呢?这么做将破坏要提供的类型安全泛型。
我们前面提到过,在编译时,泛型的类型参数被擦除成为了Object,也就是说在运行时, List< Number> 和 List< Integer>都是List<Object >,由于在运行时两者是同一种类型,因此它们之间的赋值必然会成功,但是这样的操作我们都知道是错误的,List< Number> 怎么可能与 List< Integer>相互赋值,所以,由于泛型不像组数一样存在运行时类型检查,所以它必须在编译器就将所有安全隐患消除。这就是泛型不提供协变的原因。


通配符

在使用泛型类的时候,

既可以指定一个具体的类型,如 List 就声明了具体的类型是 String;

也可以用通配符? 来表示未知类型,如 List<?> 就声明了 List 中包含的元素类型是未知的。

通配符所代表的其实是一组类型,但具体的类型是未知的。List<?> 所声明的就是所有类型都是可以的。但是 List<?> 并不等同于 List。

List<Object> 实际上确定了 List 中包含的是 Object 及其子类,在使用的时候都可以通过 Object 来进行引用。而 List<?> 则其中所包含的元素类型是不确定。其中可能包含的是 String,也可能是 Integer。如果它包含了 String 的话,往里面添加 Integer 类型的元素就是错误的。正因为类型未知,就不能通过 new ArrayList<?>() 的方法来创建一个新的 ArrayList 对象。因为编译器无法知道具体的类型是什么。但是对于 List<?> 中的元素确总是可以用 Object 来引用的,因为虽然类型未知,但肯定是 Object 及其子类。考虑下面的代码:

public void wildcard(List<?> list) {
    
    
    list.add(1);// 编译错误 
} 

如上所示,试图对一个带通配符的泛型类进行操作的时候,总是会出现编译错误。其原因在于通配符所表示的类型是未知的。

这就是三句话总结JAVA泛型通配符(PECS)中的第一句话:"?"不能添加元素,只能作为消费者

因为对于 List<?> 中的元素只能用 Object 来引用,在有些情况下不是很方便。在这些情况下,可以使用上下界来限制未知类型的范围。 如 List<? extends Number> 说明 List 中可能包含的元素类型是 Number 及其子类。而 List<? super Number> 则说明 List 中包含的是 Number 及其父类。当引入了上界之后,在使用类型的时候就可以使用上界类中定义的方法。比如访问 List<? extends Number> 的时候,就可以使用 Number 类的 intValue 等方法。

类型系统

在 Java 中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如 String 继承自 Object。根据里氏替换原则,子类是可以替换父类的。当需要 Object 类的引用的时候,如果传入一个 String 对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的(数组是协变的)。 String[] 可以替换 Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。正如前面提到的 List 是不能替换掉 List 的。

引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List 和 List 这样的情况,类型参数 String 是继承自 Object 的。而第二种指的是 List 接口继承自 Collection 接口。对于这个类型系统,有如下的一些规则:

相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即 List 是 Collection 的子类型,List 可以替换 Collection。这种情况也适用于带有上下界的类型声明。
当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对 Collection<? extends Number> 来说,其子类型可以在 Collection 这个维度上展开,即 List<? extends Number> 和 Set<? extends Number> 等;也可以在 Number 这个层次上展开,即 Collection 和 Collection 等。如此循环下去,ArrayList 和 HashSet 等也都算是 Collection<? extends Number> 的子类型。
如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。

上界

下面就是上界通配符(Upper Bounds Wildcards)

Plate<extends Fruit>

一个能放水果以及一切是水果派生类的盘子

再直白点就是:啥水果都能放的盘子,这和我们人类的逻辑就比较接近了

Plate<? extends Fruit>和Plate最大的区别就是:Plate<? extends Fruit>是Plate及Plate的基类

直接的好处就是,我们可以用“苹果盘”给“水果盘”赋值了。

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

再扩展一下,食物分成水果和肉类,水果有苹果和香蕉,肉类有猪肉和牛肉,苹果还有两种青苹果和红苹果。

//Lev 1
class Food{
    
    }
 
//Lev 2
class Fruit extends Food{
    
    }
class Meat extends Food{
    
    }
 
//Lev 3
class Apple extends Fruit{
    
    }
class Banana extends Fruit{
    
    }
class Pork extends Meat{
    
    }
class Beef extends Meat{
    
    }
 
//Lev 4
class RedApple extends Apple{
    
    }
class GreenApple extends Apple{
    
    }

在这个体系中,上界通配符Plate<? extends Fruit>覆盖下图中蓝色的区域。
在这里插入图片描述

下界

相对应的下界通配符(Lower Bounds Wildcards)

Plate<? super Fruit>

表达的就是相反的概念:一个能放水果以及一切是水果基类的盘子。

Plate<? super Fruit>是Plate的基类,但不是Plate的基类

对应刚才那个例子,Plate<? super Fruit>覆盖下图中红色的区域。
在这里插入图片描述

上下界通配符的副作用

边界让Java不同泛型之间的转换更容易了。但不要忘记,这样的转换也有一定的副作用。那就是容器的部分功能可能失效。

还是以刚才的Plate为例。我们可以对盘子做两件事,往盘子里set( )新东西,以及从盘子里get( )东西。

class Plate<T>{
    
    
    private T item;
    public Plate(T t){
    
    item=t;}
    public void set(T t){
    
    item=t;}
    public T get(){
    
    return item;}
}

上界<? extends T>不能往里存,只能往外取

  1. <? extends Fruit>会使往盘子里放东西的set( )方法失效,但取东西get( )方法还有效
  2. 取出来的东西只能存放在Fruit或它的基类里面,向上造型。

比如下面例子里两个set()方法,插入Apple和Fruit都报错。

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

//不能存入任何元素
p.set(new Fruit());    //Error
p.set(new Apple());    //Error
 
//读取出来的东西只能存放在Fruit或它的基类里。
Fruit newFruit1=p.get();
Object newFruit2=p.get();
Apple newFruit3=p.get();    //Error

编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道,因此取出来的时候要向上造型为基类。

可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:capture#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号capture#1。

然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个capture#1匹配,所以就都不允许。

所以通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。

比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer…

public <T> List<T> fill(T... t);

但通配符<?>没有这种约束,Plate<?>单纯的就表示:盘子里放了一个东西,是什么我不知道。

下界<? super T>不影响往里存,但往外取只能放在Object对象里

  1. 使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。
    因为规定的下界,对于上界并不清楚,所以只能放到最根本的基类Object中
  2. set( )方法正常。
Plate<? super Fruit> p=new Plate<Fruit>(new Fruit());
 
//存入元素正常
p.set(new Fruit());
p.set(new Apple());
 
//读取出来的东西只能存放在Object类里。
Apple newFruit3=p.get();    //Error
Fruit newFruit1=p.get();    //Error
Object newFruit2=p.get();

因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。

既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以。

但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。

PECS原则

最后看一下什么是PECS(Producer Extends Consumer Super)原则,已经很好理解了。

Producer Extends 生产者使用Extends来确定上界,往里面放东西来生产

Consumer Super 消费者使用Super来确定下界,往外取东西来消费

1、频繁往外读取内容的,适合用上界Extends,即extends 可用于的返回类型限定,不能用于参数类型限定。

2、经常往里插入的,适合用下界Super,super 可用于参数类型限定,不能用于返回类型限定。

3、带有 super 超类型限定的通配符可以向泛型对象用写入,带有 extends 子类型限定的通配符可以向泛型对象读取


本章中关于通配符的一章取自https://blog.csdn.net/ystyaoshengting/article/details/86674481

猜你喜欢

转载自blog.csdn.net/qq_33905217/article/details/109241296