下面代码是编译不通过的:
list1.add(new Father());//error list1.add(new Son());//error
- 1
- 2
原因是编译器只知道容器内是Father或者它的派生类,但具体是什么类型不知道。可能是Father?可能是Son?也可能是LeiFeng,XiaoMing?编译器在看到后面用Father赋值以后,集合里并没有限定参数类型是“Father“。而是标上一个占位符:CAP#1,来表示捕获一个Father或Father的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Son或者LeiFeng或者Father编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。
所以通配符和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。
public <T> List<T> fill(T... t);
- 1
但通配符没有这种约束,
List
单纯的就表示:集合里放了一个东西,是什么我不知道。
所以这里的错误就在这里,List
里什么都放不进去。
List list
不能进行add,但是,这种形式还是很有用的,虽然不能使用add方法,但是可以在初始化的时候一个Season指定不同的类型。比如:
List<? extends Father> list1 = getFatherList();//getFatherList方法会返回一个Father的子类的list
另外,由于我们已经保证了List中保存的是Father类或者他的某一个子类,所以,可以用get方法直接获得值
List<? extends Father> list1 = new ArrayList<>(); Father father = list1.get(0);//读取出来的东西只能存放在Father或它的基类里。 Object object = list1.get(0);//读取出来的东西只能存放在Father或它的基类里。 Human human = list1.get(0);//读取出来的东西只能存放在Father或它的基类里。 Son son = (Son)list1.get(0);
下界不影响往里存,但往外取只能放在Object对象里
下界用super进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object。
//super只能添加Father和Father的子类,不能添加Father的父类,读取出来的东西只能存放在Object类里 List<? super Father> list = new ArrayList<>(); list.add(new Father()); list.add(new Human());//compile error list.add(new Son()); Father person1 = list.get(0);//compile error Son son = list.get(0);//compile error Object object1 = list.get(0);
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Father的基类,那往里存粒度比Father小的都可以。出于对类型安全的考虑,我们可以加入Father对象或者其任何子类(如Son)对象,但由于编译器并不知道List的内容究竟是Father的哪个超类,因此不允许加入特定的任何超类(如Human)。而当我们读取的时候,编译器在不知道是什么类型的情况下只能返回Object对象,因为Object是任何Java类的最终祖先类。但这样的话,元素的类型信息就全部丢失了。
PECS原则
最后看一下什么是PECS(Producer Extends Consumer Super)原则,已经很好理解了
- 频繁往外读取内容的,适合用上界Extends
- 经常往里插入的,适合用下界Super
总结
- extends 可用于返回类型限定,不能用于参数类型限定(换句话说:? extends xxx 只能用于方法返回类型限定,jdk能够确定此类的最小继承边界为xxx,只要是这个类的父类都能接收,但是传入参数无法确定具体类型,只能接受null的传入)。
- super 可用于参数类型限定,不能用于返回类型限定(换句话说:? supper xxx 只能用于方法传参,因为jdk能够确定传入为xxx的子类,返回只能用Object类接收)。
- ? 既不能用于方法参数传入,也不能用于方法返回。
带有super超类型限定的通配符可以向泛型对象中写入,带有extends子类型限定的通配符可以向泛型对象读取。