这几个例子让你理解泛型的通配符

泛型

泛型是在JDK 5中引入的特性,是定义类和接口时使用的类型参数。
Java实现泛型的方式属于伪泛型,也就是说编译器并不会特性的类型参数重新生成一个新的类,因此对于JVM而言,不管类型参数是什么,都只保存一个该类的Class对象。
因此,Java的泛型只在编译时期提供了类型安全的校验,在编译结束后,JVM得到类便不再保留泛型的信息,这一过程称之为泛型型擦除。

泛型中的通配符

?在泛型中作为通配符使用,表示某种未知的类型参数。可以看作是一个占位符。
例如List<?>表示类型参数为某一类型的List。可以是List<String>,也可以是List<Integer>,设置可以是List<Object>

注意,一旦?确定后,就表示唯一的一个确定的类型。理解这一点对我们理解限定通配符很重要。

限定通配符——上界与下界

限定通配符是指?extends,super关键字搭配,用来限制可接受的类型参数范围。
其中,<? extends T>为上界通配符,表示泛型参数只可接受TT的子类。
<? super T>表示为下界通配符,表示泛型参数只可以为T或是T的父类。

我们用一个例子来验证限定通配符会在编译时会泛型的类型参数做校验。
用作泛型参数的类及其继承关系定义如下:

//顶层接口 植物
interface Plant{

}

// 父类 食物
class Food{

}

// 水果 继承Food并实现Plant
class Fruit extends Food implements Plant {

}
//苹果 具体的水果类型
class Apple extends Fruit{

}
//香蕉 具体的水果类型
class Banana extends Fruit{

}

泛型类如下:

//盘子 用来装东西
class Plate<T>{
    T t;
    T get(){
        return t;
    }

    void add(T t){
        this.t = t;
    }
}

第一个例子:

        //泛型参数为Apple的Plate
        Plate<Apple> applePlate = new Plate<>();
        //泛型参数为Fruit的Plate
        Plate<Fruit> fruitPlate = new Plate<>();
        //泛型参数为Food的Plate
        Plate<Food> foodPlate = new Plate<>();
        
        //声明一个 上界限定通配符的Plate引用,其上界为Fruit
        Plate<? extends Fruit> upBoundsFruitPlate;
        //让引用指向applePlate 
        upBoundsFruitPlate = applePlate;
        //让引用指向fruitPlate
        upBoundsFruitPlate = fruitPlate;
        
        //compile error
        //当用引用指向foodPlate时,编译器报类型错误
        //upBoundsFruitPlate = foodPlate;

        //声明一个下界限定通配符的Plate引用,其下界为Fruit
        Plate<? super Fruit> downBoundsFruitPlate;
        //让引用指向foodPlate OK
        downBoundsFruitPlate = foodPlate;
        //让引用指向fruitPlate OK 
        downBoundsFruitPlate = fruitPlate;
        //compile error
        //当引用指向applePlate时,编译器报类型错误
        //downBoundsFruitPlate = applePlate;

第一个例子,我们验证了上下界统配符限制泛型类型参数的规则。在强调一次,上界通配符表示只能接受T及其子类类型。下界通配符表示只能接受T及其超类类型

为什么上界只取不放,下界只放不取?

对于泛型的限定通配符而言,最让人困惑的一点就是限定通配符的存取规则——上界只取不放,下界只放不取。

首先,先要理解什么是上界只取不放,下界只放不取的限制。
上界只取不放是指当操作一个泛型参数为上界通配符<? extends T>的引用时,只能操作其返回泛型参数相关的方法(返回值为T的方法)而无法操作设置泛型参数相关(修改T)的方法
而下界只存不取则是指当操作一个泛型参数为下届通配符<? super T>的饮用时,只能操作设置泛型参数相关(修改T)而无法操作返回泛型参数相关的方法(返回值为T的方法)

为了验证上面的观点,我们在举一个例子。

        /******SCENE 1 UP BOUNDS **********/
        //声明一个泛型是上界限定通配符的引用
        Plate<? extends Fruit> p1;
        //使其指向泛型类型为Apple的引用
        p1 = new Plate<Apple>();
        //尝试从Plate中拿出水果 OK
        Fruit f = p1.get();
        //Compile Error
        //尝试往盘子中放苹果 Error
        //p1.add(new Apple());
        
        /*****SCENE 2 DOWN BOUNDS**********/
        //定义一个泛型是下界限定通配符的引用
        Plate<? super  Fruit> p2;
        //使其指向泛型为Fruit的引用
        p2 = new Plate<Fruit>();
        //尝试添加Apple OK
        p2.add(new Apple());
        //Compile Error
        //尝试从盘子中取出食物 Error
        //Food food = p2.get();

可以看到泛型类型为上界通配符Plate<? extends Fruit> p1,p1.get()方法编译是能通过的,但是p1.add(new Apple())编译器会报错。

而泛型类型为下界通配符Plate<? super Fruit> p2正好相反,p2.get()编译器会报错,但是p2.add(new Apple())是能成功运行的。

这时候,可能读者一脸懵逼的问,p1追根揭底不是指向Plate<Apple>的引用嘛,怎么p1.add(new Apple())还不允许了呢?没天理了不成?

其实,代码是最讲规则和道理的。接下来我们就好好讲讲个中缘由。
首先,Java中的泛型只是在编译时提供类型的校验,在编译接受后,就不再有泛型的信息了。比如,针对上文的Plate类,我们可以通过反编译查看其泛型擦除后的状态(注意太高级的反编译工具依旧可能从编译后的注释信息中恢复泛型信息,因此这里要用低级一些的反编译工具,比如我用了JAD):

//针对Plate的反编译
class Plate
{

    Plate()
    {
    }

    Object get()
    {
        return t;
    }

    void add(Object t)
    {
        this.t = t;
    }

    Object t;
}

再看上述从P1中取值的例子反编译结果

Plate p1 = new Plate();
Fruit f = (Fruit)p1.get();

由于Java泛型仅提供编译时期的校验,编译后不再有泛型相关信息。因此Plate在编译后,内部的泛型类型T转换成了Object类型。
但是由于编译器已经对类型安全做了校验,因此在对Plate取值时,可以将Object强转成Fruit,而不必担心出现类型转换错误。

但是泛型通配符具体表示哪一个泛型类型却是运行时确定的,因此为了确保安全,编译器对泛型通配符的校验尤其严格。考虑以下情况:

    public void getFood(String favoriate){
        Plate<? extends Fruit> p1;
        if("Apple".equals(favoriate)){
            p1 = new Plate<Apple>();
        }else if("Banana".equals(favoriate)){
            p1 = new Plate<Banana>();
        }
        // ...
    }

p1既可以指向Plate<Apple>,也可以指向Plate<Banana>,而这一过程是发生在运行时期的,因此编译器为了类型安全,是不允许p1.add(new Apple())的代码通过编译的。因为万一p1实际上指向的Plate<Banana>,那么就有可能在一个只允许存放Banana的盘子中放入了Apple,这样就相当于破坏了泛型的类型约束。
理解了这一点后,下界通配符的存取规则也就能理解了。同学们可以顺着这个思路自己思考一下。

说一些题外话,我之前对限定通配符的存取限制很难理解是因为我有一个理解的误区。拿List<? extends Fruit> list为例,我以前对这个限定通配符的中只能放的是Fruit及其子类的元素理解是这个list可以同时存放Apple,Banana等。这是一个误区,因为通配符?其实是一个占位符,一旦确定了具体类型后,它便只能是那一种具体的类型,比如说一旦确定了泛型类型是Apple,它就只能存Apple。不知道有没有同学和我犯一样的错误。

为什么要使用限定通配符

看到这里有同学会说,限定通配符的泛型类限制了存取只能二选一,那它还有啥用啊?不能存值还有啥取值的必要?存了值又取不出来得多憋屈啊?

其实,限定通配符还是有作用的,例如下面这个方法:

    public void eatFruit(Plate<? extends Fruit> plate){
        Fruit f = plate.get();
        //eat fruit
    }

这时候限定通配符就起到了限制传入参数类型的作用。假设有一个参数是Plate<Plant> p,编译器就能阻止p传入方法,也就避免了我们吃草的尴尬。

原文地址:https://www.cnblogs.com/insaneXs/p/12859408.html