Java 泛型(2):通配符

在泛型参数表达式中的问号,即为通配符

1.    泛型的不可协变性

我们在使用数组的时候,常常将子类的对象放到一个父类的数组引用中,例如:

       Fruit []fruit = new Apple[SIZE];  //ok

       fruit[0] = new Apple();  //ok

       fruit[1] = new Fruit();   //compile ok, run error

       fruit[2] = new Banana();       //comole ok, run error

 

因为Apple是Fruit的子类,所以上面的写法是完全正确的。但是将Fruit的对象赋值到fruit[1]中,因为fruit本来就是一个Fruit类型所以编译期这是没有任何问题的,但是因为运行中的数组实际类型是Apple所以,后面两种写法在运行时将会报类型错误。

 

将类似的情况用于泛型容器:

List<Fruit> fruitList = new ArrayList<Apple> //compile error

第一次阅读这段代码的时候,可能会认为这是向上转型,然而实际上,这根本不是向上转型,问题的根源在于Apple的list并不是Fruit的list,即使Apple是Fruit的子类,因为此处我们讨论的是容器的类型,而不是容器持有的类型。这一点有别于数组,因为Java的数组是协变的,而泛型是不可变的。

       所谓的数组协变指的是:如果Base是Sub的基类,那么Base[]就是Sub[]的基类,而泛型是不可变的,所以List<Base>不是List<Sub>的基类。

2.    通配符的使用与限制

由于泛型的不可协变性,所以类似List<Fruit> fruitList = new ArrayList<Apple>的用法将会出现编译错误,然而有时我们确实又需要建立这种类型间的向上转型关系,而这正是通配符的用武之地。此时我们可以有如下声明:

List<? extends Fruit> fruitList = new ArrayList<Apple>

此时我们就可以使用fruitList了,代码如下:

List<? extends Fruit> fruitList = new ArrayList<Apple> (); //ok

fruitList.add(new Apple()); //compile error

fruitList.add(new Fruit()); //compile error

fruitList.add(new Banana()); //compile error

Fruit fruit = fruitList.get(0); //ok

 

此时,声明成功了,同时我们也可以让fruitList指向一个List<Banana>。但是我们却无法向里面添加任何元素,即使是Apple类型,你也无法添加。然后,虽然不能添加任务元素,我们却可以调用get方法返回一个元素。

造成这种诡异的现象的原因是什么呢?回到最初的泛型的定义上,我们定义了一个

<? extends Fruit>,我们可能会把它读作“任何从Fruit继承的类型”,所以我们认为向其中加入Fruit(或者其子类)是没有问题的,然而,在编译器看来,这个定义的含义是却是“某种从Fruit继承的类型”,从这个定义中,编译器只知道它是Fruit的子类,但是却不知道神哪个具体的子类,所以编译器拒绝接收任何类型的Fruit,看上去好像很奇怪,然而事实上就是如此。

对于任何使用泛型的类来说,因为泛型的不可协变性,我们不能将ClassA<Sub>向上转型为ClassA<Base>,但是却可以向上转型为ClassA<? Extends Base>,但是这种转型是有代价的,这将会使编译器丢失类型信息,编译器将只知道我们的类型是Base的子类, 不知道是什么子类,所以它会拒绝任何参数为<? extends Base>的操作,例如add(<? extends Base> itme)方法。而类似get()的方法并不会有任何问题,因为此时你只需要满足get()出来的对象是Base(或者其子类)就行。

3.    超类型通配符

超类通配符是声明由某个特定类的任何基类来界定,使用<? super Sub>,或者使用

<? super T>,(不能声明<T super Sub>),使用超类型通配符就可以安全的传递一个类型到泛型中,因此我们可以往List<? super Apple> apples中执行add操作:

      apples.add(new Apple());//ok

      apples.add(new FushiApple()); //ok (FushiApple extends Apple)

      apples.add(new Fruit()); //compile error

使用了超类型通配符后,<? super T>的含义是“某种Apple的父类,但是不知道具体是什么类”,此时我们向里面add(new Apple())和add(new FushiApple())是完全合法的行为,因为虽然编译器不知道类型参数到底是什么类型,但是可以保证它一定是Apple的父类,那么Apple通过向上转型一定会满足类型要求(虽然编译器不知道是什么类型,但是知道Apple一定是这个类型)。同理,对于 Apple的子类,我们都可以使用add(),因为他们向上转型一定会是正确的类型。

我们可以说此时Apple就是泛型的下界,在这个界限往下继承都是一定满足的,而往上却不一定了,如上例所示的add(new Fruit())编译失败,因为我们可以说Apple(极其子类)通过向上转型一定是某种类(<? super T>),但是却不能说Fruit一定是某种类(<? super T>)。

       打个比方,例如有个继承链:Thing->Plant->Fruit->Apple->FushiApple,我们声明了

<? super Apple>,那么此时编译器获取的类型参数就是:“某类,它是Apple的父类,但是具体是什么不知道,有可能是Thing、 Plant或Fruit甚至其他的”。

此时如果我们传入Apple极其子类就是完全合法的,因为不管是Thring、Plant还是其他的什么类,Apple都是其子类,这也是为什么称Apple为下界的原因。

另一方面,如果传入Apple左边的类,那此时就有问题了,因为编译器能获取的信息只有Apple的父类,但是具体的类型是不知道的,例如传入Plant,然而实际上参数类型可能为Fruit(此时合法),也有可能是Thing(此时不合法),所以会处于可能合法,可能不合法的状态,因此编译器是禁止此类操作的。

小结:对于<? Extends Base>,可以使用Base base = get()的用法,因为可以确保get的对象一定是Base(或其子类),然而禁止add()任何对象,对于<? Super Sub>,可以使用add()加入Sub极其子类,但是get()操作却受到限制,因为并不知道get到的是什么类型,因此唯一合法的类型只有Object。

4.    无界通配符

所谓的无界通配符指的就是<?>,前面已经介绍了<? Extends Base>和<? Super Sub>,那么其实<?>也能理解了。<?>表示“匹配任意的一种类型,但是编译器并不知道是哪一类”,笔者认为<?>和<? extends Object>等价,因为所有类都最终继承于Object:

List<?> list = new ArrayList<Fruit>; //ok

list = new ArrayList<Apple>; //ok

 

然而,如同之前我们分析的一样,<?>虽然表示任意类型,但是编译器不知道具体是哪一类,所以我们无法向其中add()任何元素,哪怕是Object。使用<?>的意义就在于告诉编译器我这里需要使用泛型,但是我现在还没想好参数类型是什么。

5.    原生类型 & <Object>

原生类型

指类似List这种不带泛型参数的定义,原生类型是Java还没引入泛型的时候的用法,使用原生类型,如下代码,原生类型可以add()任何类型,因为从实现上来说,原生类型的内部存储类型是Object,所以理论上可以接受任何类型,下列代码可以编译通过(但会有编译告警),然而在运行时,我们get()的元素实际类型却不太一样,所以很容易出现运行时错误。

List list = new ArrayList();

list.add(new Fruit());

list.add(new Apple());

list.add(new Object());

 

原生类型可以接受任何类型,例如如下写法都ok。

       List<?> list1;  List<? extends Fruit> list2;  List<Object> list3;

       List list = list1; //ok   list = list2; //ok list = list3; //ok

<Object>

这种其实和最基本的泛型<T>是一样的,只不过我们期望的类型是Object而已。因为泛型的不可协变性,所以虽然Object是所有类的基类,但是List<Object>却不是List<Apple>的基类。List<Object>只能接受同等类型的List,如下代码所示,因为泛型类型是Object,List<Object>和原生类型一样,可以add任何类型(有编译警告),同样的在get()的时候可能出现运行时错误。

       List<Object> list = new ArrayList();

       list = list1; //error   list = list2; //error   list = list3; //error    //list1 list2 list3 定义如上

       list.add(new Fruit());  list.add(new Apple());  list.add(new Object());

6.    泛型和数组的简单比较

l  协变性

数组是可协变的,而泛型是不可协变的,数组的协变性会导致一些运行时错误,例如前文中的:

Fruit []fruit = new Apple[SIZE]; fruit[0] = new Banana(); //compile ok,run error

而在泛型中,不会存在类似的问题,因为泛型中编译都通不过:

List<Fruit> fruitList = new ArrayList<Apple> //compile error

l  具体化和擦除

数组是具体化的,而泛型是擦除的。

数组是在运行的时候才去判断元素的真正类型,所以上述的语法在编译期是可以通过的,但是在运行时会运行错误(fruit的真正类型是Apple,而我们赋予了它Banana)。

泛型是刚好相反,在编译时会根据类型进行检查,而在运行时将类型擦除,所以上述的代码在编译期会直接报错。

7.    总结

因为一些历史原因,Java的泛型采用了擦除技术,这导致了Java的泛型不同于C++等语言(采用具体化实现)的泛型。Java的泛型是一种编译器泛型,即在编译后的二进制文件中是看不到泛型的。

知道了这些,我们再回头来看<T>、<? extends Base>、<? super Sub>、<?>、<Object>、原生类型就很好理解了,因为Java的泛型是编译器泛型,所以在实现上,因为擦除,其实类型信息都丢失了,所以在运行时,其实内部都是Object,从这一点上,他们其实没有区别。

然而,虽然在运行时一样,但是因为泛型的引入,其实是引入了许多编译期的静态检查,导致了它们行为的迥异:

l  原生类型:可以理解成跳过泛型引入的静态检查,所以它可以指向任何类型,可以执行所有的add()、get()操作。

l  <?>:含义是“任何一种类型,但是编译器不知道具体是哪一种类型”,所以<?>可以指向任何一种类型,可以执行get()操作,但是却不能add()操作(因为编译器不知道具体的类型,所以拒绝所有类型)

l  <? extends Base>:和<?>类似,只是给泛型类型加上了上界,它可以指向<Base>极其子类型,同样可以执行get(),却不能执行add()操作(因为编译器只知道需要的是Base的某一子类,但是却不知道具体是哪个子类,所以拒绝所有类型)。

l  <? super Sub>:和<?>类似,只是给泛型类型加上了下界,因为编译器需要的是Sub的某一个基类,虽然不知道具体需要哪一种基类型,但是可以知道Sub(极其子类)通过向上转型一定满足,所以可以对Sub(极其子类)执行add()操作,但是却不能执行get()操作(因为不知道具体类型,但是因为Object是所以类的基类,所以其实Object是唯一合法的)。

l  <Object>: 因为泛型的不可协变性,所以只能指向<Object>,但是因为Object是所有类得基类,所以它可以执行所有的add()、get()操作。

l  <T>:其实就是<Object>的通用形式,只能指向<T>,可以执行get()操作和针对T(极其子类)的add()操作

 

java泛型是编译器泛型,是一种语法糖,生成的二进制代码中是没有泛型的,jvm感受不到泛型。java的泛型编译生成二进制代码的时候,进行了类型的擦除,放入集合的实际上是object类型,从集合中获取对象的时候 获取的是object类型, 然后进行了强制类型转换,转换成实际的类型。

记住:关于通配符的所有疑问都在于Java的泛型只是编译期做的一些事情,编译后的二进制文件根本看不到泛型的存在,理解了这一点,就理解了Java的泛型。

 

8.    一些其他讨论

l  <? extends Base>:关于这种类型,有一点值得思考的地方,思考一下<? extends Fruit>和<? extends Apple>这两种类型是什么关系呢?其实<? Extends Apple>是<? Extends Fruit>的子类。

其实这里还有一种理解,如前分析<? extends Fruit>表示是Fruit的某一子类(ClassA),但是不知道具体是哪一种子类,<? extends Apple>表示Apple的某一子类(ClassB),但是同样不知道具体是哪一种子类,ClassA和ClassB实际的类型是未知的,那么ClassA一定是ClassB的基类吗?从这里考虑,好像<? Extends Apple>是<? Extends Fruit>的子类这一说法并不那么严谨,然而事实就是如此。

l  关于List和List<?>类型,前面说过,List<?>禁止所有add()操作,而List允许所有add()操作,考虑以下代码,如我们前面分析的,list1不能执行任何add(),然而把list1赋值到list2之后,不但可以add(new Fruit()),还可以add(new Cat()),而其指向的明明是一个ArrayList<Fruit>啊。其实这正印证了我们之前的分析,在运行时看不到泛型,都是Object,而List跳过了编译期检查,所以我们可以add()任何类型。

List<?> list1 = new ArrayList<Fruit>();

List list2;

list1.add(new Fruit()); //error

list2 = list1;

list2.add(new Fruit()); //ok

list2.add(new Cat()); //ok

 

      

 

posted @ 2019-06-17 16:11  LemonPi  阅读(435)  评论(0编辑  收藏  举报