<? extends T> 及 <? super T> 重温

<? extends T> 及<? super T> 重温

本文针对泛型中<? extends T> 及<? super T>的主要区别及使用用途进行讨论.
作者尽量描述其原理,分析疑点. 希望对复习Java泛型使用,项目架构及日常使用有帮助
也是作者作为学习的加强记忆

编码例子背景

设定有一盘子(容器),可以存放物品,同时有食物,水果等可以存放在容器里面.

import com.google.common.collect.Lists; //引入guava的Lists工具方便生产List实例
class Food {
    /** name*/
    protected String name = "Food";
    /** 打印食物名称*/
    public void echo() {
        System.out.println(name);
    }
}

class Fruit extends Food {}

class Apple extends Fruit {
    public Apple() {
        name = "Apple";
    }
}

class Pear extends Fruit {
    public Pear() {
        name = "Pear";
    }
}

class Plate<T> {
    private T item;
    public Plate() {}
    public Plate(T item) {
        this.item = item;
    }
    public void set(T t) {
        item = t;
    }
    public T get() {
        return item;
    }
    /** 模仿处理泛型T实例*/
    public void methodWithT(T t) {
        //简单打印实例
        System.out.println("methodWithT,T's is : " + t);
    }
}

引出问题背景

现在,有两个容器,一个放水果(没说明放哪种),一个特指放苹果

Fruit fruit = new Fruit();
Apple apple = new Apple();

Plate<Fruit> fruitPlate = new Plate<>();
Plate<Apple> applePlate = new Plate<>();

现在对着两个容器进行一些常规操作,赋值/调用API

Fruit fruit = new Fruit();
Apple apple = new Apple();

// 父类容器,放置子类
// 此处是多态的提供的API
// apple is a fruit
fruitPlate.set(apple);
fruitPlate.methodWithT(apple);

// 父类容器引用指向(被赋值)子类容器
// 父类容器与子类容器的关系,此处关注的是容器Plate这个类!!!
// 并没有像父子类中的继承关系(多态)!!!
// apple's plate is not a fruit's plate
// ERROR
// fruitPlate = applePlate; // 装水果的盘子无法指向装苹果
// ERROR
// applePlate = fruitPlate;// 明显错误,子类容器指向父类容器

初学Java的读者看到此处,心中必定会有疑问,难道装苹果的盘子不是装水果的盘子?很遗憾,使用来修饰泛型,编译器确实是这么认为的 所以,以上测试代码中,父类容器引用指向(被赋值)子类容器,编译报错,是跟多态上的认识是相反的结果
为了解决此类容器间'继承多态'问题,实现父子容器泛类引用指向,于是JDK提供了<? extends T><? supper T>

<? extends T>

<? extends T> : 上界通配符(Upper Bounds Wildcards),表示上界是T,用此修饰符修饰的泛类容器,可以指向本类及子类容器

示例如下:

/**
* <? extends T> 上界通配符(Upper Bounds Wildcards)
* <p>
* Plate <? extends Fruit> extendsFruitPlate 可以被 Plate <Fruit> 及 Plate <Apple> 赋值
*/
@Test
public void extendsTest() {
    Fruit fruit = new Fruit();
    Apple apple = new Apple();
    
    Plate<Fruit> fruitPlate = new Plate<>();
    Plate<Apple> applePlate = new Plate<>();
    Plate<? extends Fruit> extendsFruitPlate ;

    // SUCCESS
    // Plate<? extends Fruit>引用可以指向Plate<Fruit>以及Plate<Apple>
    extendsFruitPlate = applePlate;
    extendsFruitPlate = fruitPlate;
}

<? supper T>

<? supper T> : 下界通配符(Lower Bounds Wildcards),表示下界是T,用此修饰符修饰的泛类容器,可以指向本类及父类容器

示例如下:

/**
* <? supper T> 下界通配符(Lower Bounds Wildcards)
* Plate <? supper Fruit> superFruitPlate 可以被 Plate <Fruit> 及 Plate <Object> 赋值
*/
@Test
public void supperTest() {
    Fruit fruit = new Fruit();
    Apple apple = new Apple();

    Plate<Fruit> fruitPlate = new Plate<>();
    Plate<Apple> applePlate = new Plate<>();
    Plate<Object> objectPlate = new Plate<>();
    Plate<? super Fruit> superFruitPlate = new Plate<>();

    // SUCCESS
    // Plate<? super Fruit>引用可以指向Plate<Fruit>以及Plate<Object>
    superFruitPlate = fruitPlate;
    superFruitPlate = objectPlate;
    
    // ERROR
    // superFruitPlate = applePlate; //  <? supper Fruit>修饰的容器不能被子类容器赋值
}

以上例子说明,<? extends T><? super T>可以解决父子类泛型之间的引用问题,但同时,在使用被修饰的泛型容器的相关API,也做出了相关的调整. 可以说,这样的便利,是建立在一定的规则上,和付出一些代价的.可以肯定地是,这些限制规则,是符合多态的规则.理解后对我们工作编程上对编译器安全类型限制理解有一定的帮助

以下说明是相关调整的表现及这样的限定原因

<? extends T> 修饰的泛型其接口特点

<? extends T> 具体表现:

  1. 返回父类T的接口:调用这类型的接口返回的实例类型是父类T(这句结论说跟没说一样,理解起来特别容易.)
  2. 接收父类T的接口:这类型的接口均不可以再被调用

形象点,就如同网上绝大多数描述的一样:不能往里存,只能往外取
注:存与取,仅仅是一种表现形式,确切来说我认为是返回T接口(方法)及接收T接口(方法)更为准确
不能往里存含义是接收T接口不能再调用,否则编译异常
只能往外取含义是返回T接口可以正常使用,返回的实例类型就是T

代码表现如下:

/**
 * <? extends T> 上界通配符(Upper Bounds Wildcards)
 * 注意点: 只能获取,不能存放
 */
@Test
public void extendsAttentionTest() {
    Fruit fruit = new Fruit();
    Apple apple = new Apple();
    Pear pear = new Pear();

    Plate<? extends Fruit> extendsFruitPlate;
    extendsFruitPlate = new Plate<Fruit>(fruit);
    extendsFruitPlate = new Plate<Apple>(apple);
    extendsFruitPlate = new Plate<Pear>(pear);

    // 以下ERROR代码,尝试调用接收泛型T的方法,均编译不过
    // ERROR:
    // extendsFruitPlate.set(fruit);
    // extendsFruitPlate.set(apple);
    // extendsFruitPlate.set(pear);
    // extendsFruitPlate.set(new Object());
    // extendsFruitPlate.methodWithT(fruit);
    // extendsFruitPlate.methodWithT(apple);
    // extendsFruitPlate.methodWithT(pear);
    // extendsFruitPlate.methodWithT(new Object());
    // 以上注释的错误代码,初学者也会有疑问,
    // 为什么<Plate<? extends Fruit> extendsFruitPlate;这样装水果子类的盘子,
    // 现在什么东西都不能放了?那我还要这个容器有什么用?这是不是跟思维惯性认知有点偏差?

    // SUCCESS
    // 返回的是泛型T即Fruit,具体的实例是Pear类型
    Fruit getFruit = extendsFruitPlate.get();
    getFruit.echo();// 输出Pear

    // 接口测试
    class ExtendsClass {
        public void extendsMethod(List<? extends Fruit> extendsList) {
            // ERROR:
            // 出错原理同上,不能调用接收泛型T的方法
            // extendsList.add(fruit);
            // extendsList.add(apple);
            // extendsList.add(new Object());
            // SUCCESS
            // 获取是父类,可以强转为子类再使用
            Fruit getFruitByList = extendsList.get(0);
            getFruitByList.echo();
        }
    }
    List<Fruit> fruits = Lists.newArrayList(fruit);
    List<Apple> apples = Lists.newArrayList(apple);
    ExtendsClass extendsClass = new ExtendsClass();
    // List<? extends Fruit> extendsList可以接收List<Fruit>/List<Apple>
    extendsClass.extendsMethod(fruits);
    extendsClass.extendsMethod(apples);
}

<? extends T> 相关限制的原因

Fruit fruit = new Fruit();
Apple apple = new Apple();
Pear pear = new Pear();
Plate<? extends Fruit> extendsFruitPlate;
extendsFruitPlate = new Plate<Fruit>(fruit);
extendsFruitPlate = new Plate<Apple>(apple);
extendsFruitPlate = new Plate<Pear>(pear);

编译器的理解: Plate<? extends Fruit> extendsFruitPlate 这个盘子 :

  • 你不能保证读取到 Apple ,因为 extendsFruitPlate 可能指向的是 Plate<Fruit>
  • 你不能保证读取到 Pear ,因为 extendsFruitPlate 可能指向的是 Plate<Apple>
  • 你可以读取到 Fruit ,因为 extendsFruitPlate 要么包含 Fruit 实例,要么包含 Fruit 的子类实例.
  • 你不能插入一个 Fruit 元素,因为 extendsFruitPlate 可能指向 Plate<Apple>Plate<Pear>
  • 你不能插入一个 Apple 元素,因为 extendsFruitPlate 可能指向 Plate<Fruit>Plate<Pear>
  • 你不能插入一个 Pear 元素,因为 extendsFruitPlate 可能指向 Plate<Fruit>Plate<Apple>

你不能往Plate<? extends T>中插入任何类型的对象,因为你不能保证列表实际指向的类型是什么,
你并不能保证列表中实际存储什么类型的对象,唯一可以保证的是,你可以从中读取到T或者T的子类.

所以,

  • 可以调用接收泛型T的方法的接口 extendsFruitPlate.get() 获取 Fruit 的实例
  • 却不能调用接收泛型T接口 extendsFruitPlate.set(fruit)添加任何元素
  • 也不能调用接收泛型T接口 extendsFruitPlate.methodWithT(fruit)处理任何对象

extendsFruitPlate指向 Plate<Pear>时候, 调用extendsFruitPlate.set(fruit)extendsFruitPlate.methodWithT(fruit)调用等价于

Apple apple = new Apple();
Plate<Pear> pearPlate=new Plate<Pear>();
// 以下明显类型不相同,且不符合多态,导致类型转换异常
pearPlate.set(apple);
pearPlate.methodWithT(apple); 

可以说,<? extends T> 修饰的泛型容器可以指向子类容器,是建立在不能调用接收泛型T的方法条件上的,否则运行时将可能产生类型转换异常
编译器总是往最安全的情况考虑,尽量把可能存在的问题在编译期间就反映出来.所以编译器在处理<? extends T> 修饰的泛型容器时候,干脆让这个容器得接收泛型T方法不能再调用了

<? super T> 修饰的泛型其接口特点

<? super T> 具体表现

  1. 返回父类T的接口:调用这类型的接口返回的类型是Object类型
  2. 接收父类T的接口:调用这类型的只能传入父类T及T的子类实例

形象点,就如同网上绝大多数描述的一样:不影响往里存,但是往外取只能放在 Object
注:存与取,仅仅是一种表现形式,确切来说我认为是返回T接口(方法)及接收T接口(方法)更为准确
不影响往里存含义:调用这类型的只能传入父类T及T的子类实例
往外取只能放在 Object含义:调用这类型的接口返回的类型是Object类型,可以通过强转手段转化为子类

代码表现如下:

/**
 * <? supper T> 下界通配符(Lower Bounds Wildcards)
 * 注意点: 取出是Object,存放是父类或子类
 */
@Test
public void superAttentionTest() {
    Object object = new Object();
    Food food = new Food();
    Fruit fruit = new Fruit();
    Apple apple = new Apple();
    Pear pear = new Pear();

    Plate<? super Fruit> superFruitPlate;
    // 可以被 Plate<Object> , Plate<Food> ,Plate<Fruit> Plate<父类容器> 赋值
    superFruitPlate = new Plate<Object>(object);
    superFruitPlate = new Plate<Food>();
    superFruitPlate = new Plate<Fruit>();

    // SUCCESS
    superFruitPlate.set(fruit);
    superFruitPlate.set(apple);
    superFruitPlate.set(pear);
    superFruitPlate.methodWithT(fruit);
    superFruitPlate.methodWithT(apple);
    superFruitPlate.methodWithT(pear);
    // ERROR:接收父类T的接口,当[不是 T或T的子类时],则编译异常
    // superFruitPlate.set(food);
    // superFruitPlate.set(object);
    // superFruitPlate.methodWithT(food);
    // superFruitPlate.methodWithT(object);
    // 以上注释的错误代码,初学者也会有疑问,
    // 为什么<Plate<? super Fruit> superFruitPlate;这样可以指向水果父类的盘子,
    // 现在却只能放子类?这是不是跟思维惯性认知有点偏差?

    // 只能获取到Object对象,需要进行强转才可以进行调用相关API
    Object supperFruitPlateGet = superFruitPlate.get();
    if (supperFruitPlateGet instanceof Fruit) {
        // 为什么需要 instanceof ? 
        // superFruitPlate可以指向Plate<Food>,获取出来实际是Food实例
        Fruit convertFruit = (Fruit) supperFruitPlateGet;
        convertFruit.echo();
    }

    // 接口测试
    class SuperClass {
        public void supperMethod(List<? super Fruit> superList) {

            superList.add(fruit);
            superList.add(apple);
            superList.add(pear);
            // ERROR:原因如上,调用method(T t)时候,当t[不是 T或T的子类时],则编译异常
            // superList.add(object);
            // superList.add(food);
            Object innerObject = superList.get(0);
            if (innerObject instanceof Fruit) {
                // 为什么需要 instanceof ? 
                // 像这样:superFruitPlate 可以指向List<Object> objects,获取出来是Object
                Fruit innerConvertFruit = (Fruit) innerObject;
                innerConvertFruit.echo();
            } else {
                System.out.println("supperMethod:非Fruit,插入非本类或非子类:" + innerObject);
            }
        }
    }
    List<Object> objects = new ArrayList<>(Arrays.asList(object));
    List<Food> foods = new ArrayList<>(Arrays.asList(food));
    List<Fruit> fruits = new ArrayList<>(Arrays.asList(fruit));
    List<Apple> apples = new ArrayList<>(Arrays.asList(apple));
    List<Pear> pears = new ArrayList<>(Arrays.asList(pear));
    SuperClass superClass = new SuperClass();
    superClass.supperMethod(objects);
    superClass.supperMethod(foods);
    superClass.supperMethod(fruits);
    // ERROR 原因同上,非Fruit及Fruit父类容器则编译不通过
    // superClass.supperMethod(apples);
    // superClass.supperMethod(pears);
}

<? super T> 相关限制的原因

Plate<? super Fruit> superFruitPlate;
// 可以被 Plate<Object> , Plate<Food> ,Plate<Fruit> Plate<父类容器> 赋值
superFruitPlate = new Plate<Object>(object);
superFruitPlate = new Plate<Food>();
superFruitPlate = new Plate<Fruit>();

编译器的理解: Plate<? super Fruit> superFruitPlate 这个盘子 ,

  • superFruitPlate 不能确保读取到 Fruit ,因为 superFruitPlate 可能指向 Plate<Object>Plate<Food>
  • superFruitPlate 不能确保读取到 Food ,因为 superFruitPlate 可能指向 Plate<Object>

所以,取出来必须是Object,最后需要则调用强转

  • 你不能插入一个 Food 元素,因为 superFruitPlate 可能指向 Plate<Fruit>
  • 你不能插入一个 Object 元素,因为 superFruitPlate 可能指向Plate<Fruit>Plate<Food>
  • 你可以插入一个 Fruit/Apple/Pear 类型的元素,因为 Fruit/Apple/Pear 类都是 Fruit,Food,Object的本类或子类

所以,从 superFruitPlate 获取到的都是Object对象,superFruitPlate 插入的都是Fruit的本类或本身

故有如下结论:

  • superFruitPlate 调用返回父类T的接口,获取到的都是 Object 对象;
  • superFruitPlate 调用接收父类T的接口,只能传入父类T及T的子类实例

当 superFruitPlate 指向 Plate ,
调用 superFruitPlate.set(food) 和
superFruitPlate.methodWithT(food)
调用等价于:

Plate<Fruit> pearPlate=new Plate<Fruit>();
Food food=new Food();
// 以下明显类型不相同,且不符合多态,导致类型转换异常
fruitPlate.set(food);
fruitPlate.methodWithT(food);

可以说,<? super T> 修饰的泛型容器可以指向父类容器,是建立在调用接收T的接口,只能传入T及T的子类实例条件上的,否则运行时将可能产生类型转换异常
编译器总是往最安全的情况考虑,尽量把可能存在的问题在编译期间就反映出来.所以编译器在处理<? super T> 修饰的泛型容器时候,干脆让这个容器得接收泛型T方法只能传入T及T的子类

PECS (Producter Extends, Consumer Super) 原则

以上原则来源两者主要区别,合理使用其优点,有种去其糟粕,取其精华的意思

  • <? extends T> : 可以获取父类,向外提供内容,为生产者角色
  • <? super T> : 可以调用接收/处理父类及子类的接口,为消费者角色

举一个JDK 中例子:

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                // si 来源List<? super T> dest,向外提供内容,生产者
                // di 来源List<? extends T> src,接收/处理类型T的本类或子类,消费者
                di.set(si.next());
            }
        }
    }

小结

至此,本文完结了.重温的时候,发现很多之前想当然的结论,并没有细细研究其中原因.现在理解起来是不会再次忘记的了.要记住的是需要理解编译器是怎么认为的而不是怎么从修饰符去片面理解

通篇显得有点啰嗦,至少作者认为把重点及原因说清楚了.以上如有不当之处敬请指正.

参考

本文例子来源主要有二,最精髓的地方是StackOverflow的链接的第一第二个回答

博客园 : RainDream : <? extends T>和<? super T>

Stackoverflow:Difference between<? super T>and<? extends T>in Java

posted @ 2019-08-23 10:31 oneisall8955 阅读(...) 评论(...) 编辑 收藏