1. 泛型类型参数的多态问题:
1) Java泛型有一个很奇怪的特点:那就是如果A是B的父类,那么泛型类Generic<A>却不是Generic<B>的父类,这就意味着不能把Generic<B>的对象赋给Generic<A>的引用了!!
!!和泛型不一样的是数组就不存在这个问题,如果A是B的父类,那么A[]也是B[]的父类,在多态赋值的时候就不存在任何问题;
2) 可是Java的泛型为什么要这样设定呢?就是源于数组的缺陷,Java这样做就是为了弥补数组里犯的问题:
i. 看以下代码:
Integer[] iarr = new Integer[5];
Number[] narr = iarr;
narr[0] = 1.5;
!!Java中这是完全没有问题,编译时完全正确,但是运行时抛出异常了,因为narr实际上在运行时是一个Integer[]引用,它只能放入int型数,但是最后塞了一个double(1.5)型的数,显然类型冲突了,因此抛出异常了!最恶心的是抛出的是ArrayStoreException而不是类型转化的ClassCastException,因此如果你光是从运行时的报错信息你还很难判断是类型冲突的问题!!
!!这个问题正是由于数组的多态问题造成的,Number是Integer的父类,因此Number[]也是Integer[]的父类;
ii. 正因为上面的原因,Java规定泛型不允许出现上述的问题,即如果A是B的父类,那么Generic<A>绝对不是Generic<B>的父类!!因此上面的例子如果换成是泛型的话就是,Generic<Number>不是Generic<Integer>的父类,不能把Generic<Integer>对象赋给Generic<Number>的引用,如果这样做就会发生编译错误!!!
!!这种编译错误是一种语法上的错误!!!Java在语法层面上就避免了这个问题;
iii. Java对泛型的要求就是,如果在编译时没有发生警告(或错误),那么在运行时绝对不会发生类型转化异常!!!!!可见其人性化程度之高!!!
!!但可惜的是,数组还是没能实现这个要求,因为数组设计地太早,是作为Java语言的一部分诞生的,太原始太底层,无法修改了!!所以使用数组的时候一定要格外小心多态问题!!
2. 类型参数的多态问题引出了另一个非常头疼的问题——方法传参问题:使用类型通配符(?)
1) 假设现在有一个方法:void func(List<?> list) { ... } // 我想让这个list参数的类型参数是不确定的,可以是任意类型,如果实参传的是String那模板就是String的,如果是Integer那模板就是Integer的,如果是某个自定义类型的,那么模板就是那个自定义类型的
2) 你可能会想到这样写:void func(List<Object> list) { ... }
!!想得很美,以为参数是Object就可以接受任意类型的List<?>了?但是上面讲过了,泛型类型参数存在多态问题,比如你想传一个List<String>的参数进去,但是List<String>并不是List<Object>的子类,会直接发生编译错误!!!那该怎么办呢?
3) 就直接使用上面的?表示任意类型即可,即上面的:void func(List<?> list) { ... }就行了!!
i. 当?出现在泛型的<>中时就成为了泛型的类型通配符了!
ii. ?可以代表任意类型!!Generic<?>是所有的Generic<Xxx>的父类!!传参的时候使用任意Generic<Xxx>参数都没有任何问题!!
4) ?带来的新问题——?泛型对象是只读的,不能修改!!!
i. 如果你直接用List<?> list的list引用,对该List进行修改的话会直接引发编译错误!!!
ii. 原因其实很简单,因为?可以表示任何类型,表示任何类型的意思就是既可以表示Integer,也可以表示String,也可以表示任何自定义类型,那么你倒过来想,如果你想add一个"你好"字符串,但如果?刚好表示的是Integer,那不是类型冲突了吗?而?可以表示任何类型,你也不知道它到底表示什么类型;
!!正是因为这个原因(?可以表示任何类型),所以不允许修改?泛型对象!!因此?泛型对象是只读的!!
5) ?泛型的另一个问题:定义中所有出现泛型类型参数的地方都被替换成了?,表示未知类型:
i. 比如泛型类中有一个方法:T get(int index);
!!那么如果用?作为参数,那么该方法也会变成? get(int Index);了,即返回值也变成了一个未知类型了!!
!!包括其他所有出现T的地方,都会被替换成?未知类型!
ii. 那么现在主要的问题来了,未知类型?到底是个什么类型呢?正因为你不确定它是什么类型所以才叫未知类型,但是在实际应用过程中(特别是在调用对象方法的时候)肯定是要确定它是一个什么类型的,否则就会出现各种类型问题!
iii. 未知类型?在实际中就等于其类型上限?什么是类型上限?这里先不管,反正先记住这些类型上限即可:<?>和<? super Xxx>的类型上限都是Object,<? extends Xxx>的类型上限extends后面的Xxx;
iv. 例如:<?>和<? super String>写法中?的类型上限就是Object,因此就直接把?返回值当成Object类型就行了,而<? extends Number>中?类型的返回值就当成Number就行了!!虽然返回?类型会进行类型提升,但是多态关系还是存在的,如果调用返回值(类型是?)的子类同名覆盖方法,则调用的是子类的那个版本!示例:
class A { // 默认继承Object
@Override
public String toString() {
// TODO Auto-generated method stub
return "hahaha";
}
}
public class Test {
public void test(List<?> list) {
list.forEach(ele -> System.out.println(ele));
}
public static void main(String[] args) {
ArrayList<A> list = new ArrayList<>();
list.add(new A());
list.add(new A());
list.add(new A());
new Test().test(list); // 3个"hahaha",而不是Object类的那个toString版本!
}
}
!!注意:是把返回的?类型当做类型上限看待,而不是直接将?彻底当成类型上限来看待,因为?只能表示任意类型!!而不是一个具体的类型,只不过在使用?类型的返回值是把它看做上限是绝对安全的!!!如果不看做上限会有隐患而已!!!
!!要当成类型上限看待是因为你还不知道里面存放的具体是什么类型的参数,由于你不确定,因此为了避免错误就把?返回值当成其类型上限,但是如果你知道那就没问题,可以直接强制类型转换:
System.out.println(list.get(1).getClass()); // 运行时类型是永远可以记住的,这是Object类里就已经提供的基础功能,因此这里返回的是A
A a = (A)(list.get(1));
!接着上面的例子;
6) 小结:
i. 由于?表示任意(未知)类型,因此?泛型对象是只读的;
ii. 对于读取(返回)的?类型值,如果你已经知道其真实的运行时类型,则可以强转后使用,如果不清楚就当成?的类型上限使用是最安全的!
3. 设定?的上下限:
1) 单单一个?可以表示任意类型,但有时候需要限定?的范围,这种需求是普遍存在的,就举一个最简单的例子:
i. Shape是Circle、Rectangle、Triangle的父类;
ii. Shape中定义了一个抽象方法draw表示绘制图形,Circle、Rectangle、Triangle都有相应的draw的覆盖版本,用于绘制各自的图形;
iii. 这是一个多态的典型例子,可以用Shape的引用和任意具体图形挂钩,然后调用Circle的draw来绘制不同的图形;
iv. 考虑到一个方法drawShape(List<?> list); 想把list中的所有图形画出来,显然,这里希望?只代表Shape、Circle、Rectangle、Triangle这几种图形,其它的不要,但是现在一个?可以代表任意类型,可以是String,也可以是Integer,这显然不符合要求,但是也不能写成drawShape(List<Shape> list),因为List<Shape>并不是List<Circle>等的父类!!
2) 而受限的的?(通配符)就可以解决这个问题:不管受不受限,?仍然表示未知类型!!
i. 设定?的上限:<? extends Xxx>表示?只能代表Xxx或者Xxx的子类;
ii. 设定?的下限:<? super Xxx>表示?只能代表Xxx或者Xxx的父类;
!!按照继承的上下关系,父在子上,子在父下,因此extends划定了?的上限,而super划定了?的下限,对于super限定,其上限是无限的,而所有类的父类都可以追溯到Object,因此super限定的?的上限就是Object!!
3) 在上面的问题中就可以使用?的上限来解决:drawShape(List<? extends Shape> list); // 这里的?就只能是Shape、Circle、Rectangle、Triangle之一了,如果传其它类型就会直接编译报错!
!!这就比单用一个<?>要好很多,因为单用一个<?>需要强制类型转换:
public void drawShape(List<?> list) {
for (Object obj: list) {
Shape s = (Shape)obj; // 由于传进来的可能是任何乱七八糟的类型(比如String等,这些就是错误的),因此需要强制类型转换一下
s.draw();
}
}
public void drawShape(List<? extends Shape> list) {
for (Shape s: list) { // 由于已经保证了传进来的类型最高就是Shape,因此这里就无需强转,直接使用Shape类型多态就行了
s.draw();
}
}
!!可以看到,泛型的目的就是为了避免各种臃肿的代码(强制类型转换),这里使用?的上限就可以是代码简化很多!!
4) 不管受不受限,?仍然表示未知类型,仍然是只读的,不能修改!!
i. 虽然<? extends Shape>已经使?受限了,但是这个?泛型对象仍然是只读的,不能修改;
ii. 这个问题很好考虑,还是之前的分析方法一样,?现在可能是Shape,可能是Circle,可能是Rectangle,也可能是Triangle,具体是哪个没人知道,因为?可以表示这4个中的任意一个;
iii. 现在倒过来想,你现在想往该?泛型对象中加入一个Rectangle,那如果?代表的是Triangle,那岂不是类型冲突了吗?
iv. 所以,不管?受限不受限,只要出现了?,那么?泛型对象就是只读的,想都别想改它了!!!!
4. 类型参数的上限:
1) 除了在参数传递中,类型通配符?可以设定上下限,普通的泛型定义中,类型参数也可以设定上限,例如:public class A<T extends B>,这就表示类型参数T只能是B或者B的子类,在使用该泛型时如果用其它类型实例化就会编译报错!!
2) 可惜的,类型参数只能设定上限,不能设定下限,即没有:class A<T super B>这样的语法!!!Java目前不支持!!
3) 类型参数不仅可以指定上限,也可以指定多个必须实现的接口:
i. 类型参数的上限设定和接口限定的一般写法是:class 泛型名<T extends 上限类 & 接口1 & 接口2...>
ii. 其中最多只有一个上限类,不能同时继承多个类,这是必然的,因为Java本身就不支持多重继承;
iii. 但是Java在接口层面支持“多重继承”,因此可以实现多个接口,接口之间用&连接;
iv. 这里要求接口必须写在上限类之后,否则会编译报错的!!
v. 这里的意思就是使用该泛型的时候,实例化的具体类型必须是指定上限类或者其子类,并且必须实现指定的所有接口,不满足上述要求的实例化类型都会导致编译错误!