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