泛型边界_2 通配符

一、数组协变类型

先看一个数组的独特的行为,能够向子类型的数组赋予父类型的引用。

public class CovariantArrays {
   public static void main(String[] args) {
       Fruit[] fruit = new Apple[10];
       fruit[0] = new Apple();
       fruit[1] = new RedApple();
       // fruit[2] = new Food(); // 编译即抛异常
       fruit[3] = new Fruit();  // 编译通过,但抛出运行时异常:java.lang.ArrayStoreException:
       fruit[4] = new Orange(); //编译通过,但抛出运行时异常 :java.lang.ArrayStoreException:
   }
}
class Food{}
class Fruit extends Food{}
class Apple extends Fruit{}
class Orange extends Fruit{}
class RedApple extends Apple{}

分析可知:

  • main()第一行::创建了一个Apple数组(右边),然后把整个数组赋值给 Fruit数组的引用(左边)。一个Apple也是一种Fruit,所以一个Apple数组也是一个Fruit数组,看起来没毛病!

然后看:

  • 在编译期,编译器允许将Fruit(父类)元素放入数组
  • 在运行期,运行时 知道它实际处理的是Apple类型,或者Apple的子类型,碰到Apple类型的父类依然抛出异常

其实:

  • Fruit[] fruit = new Apple[10] 这样的写法用“Upcast(向上转型)”来形容有点不太合适。 数组的行为定位应该是“持有其他对象”。我们还是将其描述为“把一个数组赋值给另一个数组”比较恰当。 不过呢,因为我们确实干的“Upcast(向上转型)”的事儿,所以数组对象肯定保存了持有的对象的真实类型。形象地说,好似数组都知道它们实际持有的是什么,所以在编译期检查和运行时检查我们才不会误用。
    这里怎么翻译都别扭,还是用BruceEckel的原话:
    “Upcast” is actually rather a misnomer here. What you’re really doing is assigning one array
    to another. The array behavior is that it holds other objects, but because we are able to
    upcast, it’s clear that the array objects can preserve the rules about the type of objects they
    contain. It’s as if the arrays are conscious of what they are holding, so between the compiletime checks and the runtime checks, you can’t abuse them。

二、容器类型(区别容器持有的类型)

上面一节:数组中插入了不正确的类型是在运行时发现的,但是Java泛型的主要目标之一是将在运行时才能发现的错误尽量提前到编译期暴露出来。再来看看java 容器类

public class NonCovariantGenerics {

//    List<Fruit> fruits = new ArrayList<Apple>(); // compile error : incompatible types
}

起初看这段代码,你可能把这样的错误解释为:不能把一个 Apple容器赋值给一个Fruit容器。
但是别忘了,泛型可不只与容器本身有关系。这样的报错要解释为:不能把一个关于Apple的泛型赋值给一个关于Fruit的泛型。

如果像数组一样,编译器知道足够多的信息来确定类型,那可能就不会这样报错了。但是,编译器其实并不知道这些信息,因此,它拒绝了“upcast”。不过说到底这也不算Upcast 嘛– 一个Apple的List并不算一种Fruit的List,尽管一个Apple是一种Fruit。一个Apple的List能持有Apple或其子类类型,一个Fruit的List能持有Fruit或其子类类型,当然也包括Apple类型。但是,这不是说:一个AppleList就是一种Fruit List了。一个AppleList 在类型上并不能算一种FruitList。

这有点绕:
说白了,我们讨论的是容器的类型,而不是容器中元素的类型跟数组不同,泛型并没有内置的协变机制。

为啥呢?
因为数组在Java中是完全定义的,无论在编译期还是运行时都有内置的类型检查。反观泛型,编译器和运行时 根本不知道你想拿类型干什么,也不知道对应到这些类型的规则是什么。

可有时,我们还是希望能够在两个类型之间建立Upcast(向上转型)的关系,这就需要用到通配符 ? 了。

public class GenericsAndCovariance {

    public static void main(String[] args) {
        // wildcards allow covariance 通配类型允许协变
        List<? extends Fruit> list = new ArrayList<Apple>();

//        不能添加任何元素
//        list.add(new Apple());
//        list.add(new Fruit());

        Fruit fruit = list.get(0);// 能够运行
    }

首先解读一下:

List< ? extends Fruit> list = new ArrayList<Apple>() 

这句话左边可以翻译为:一个确定了元素类型是Fruit或其子类的List。注意:通配符的存在并不是说这个List的类型可以是任何Fruit或其子类(最好不要理解为 List元素的类型是Fruit或其子类),而是Fruit或其子类中特定的一个类型。 这个地方,这是个坑啊!

假如左边的唯一限制是:这个List的类型是Fruit或其子类中的其中一种,可你又不care具体是哪个,那你能拿这个List做啥呢?你都不知道List 持有的具体类型,又怎么能安全添加一个对象作为元素呢?跟前面的CovariantArrays类中向上转型数组不一样,这根本行不通,除非编译器而不是运行时阻止了向上转型这种操作的发生。

这看起来有点蛋疼,你甚至都不能像这个list添加一个Apple对象了。是的,你知道可以添加一个Apple对象(等式 右边),但是编译器不知道啊!一旦像这个语句一样执行这样的向上转型,就不能再往这个list中添加元素了,即使是Object也不行!

不过呢,当你从这个list中取元素时还是安全的,最起码你知道list里都是 Fruit嘛,不管是Fruit本身,还是Fruit子类。编译器还是知道这点的。

三、小结

  • 1、Java中数组具有协变性,但是泛型却是不行的。(可见数组在Java中其实是很特殊的,后面会逐渐介绍)
  • 2、这种本质上的差异在于::数组在Java中是完全定义的,在编译期、运行时都有内置检查;但是使用泛型时,编译期、运行时压根不知道具体怎么使用这些泛型。
  • 3、 List< ? extends Fruit> list = new ArrayList< Apple>() 这样的声明,其实就是AppleList向上的转型,注意不是Apple向上转型为Fruit。转型后,就不能加入任何元素了。

四、补充例证

下面我们用Number和Integer来证明数组的协变性:

public class ListCovariance {

    public static void main(String[] args) {
        Number[] numberArr = new Integer[10];
        numberArr[0] = 1;  //正常
//        numberArr[1] = 0.11D; //运行时异常 java.lang.ArrayStoreException:

//        List<Number> numberList = new ArrayList<Integer>(); // Incompatible types
        List<? extends Number> numbers = new ArrayList<Integer>();
    }
}

to complement :
关于协变与逆变
https://www.cnblogs.com/keyi/p/6068921.html

猜你喜欢

转载自blog.csdn.net/qq_30118563/article/details/82289641