问题1. B继承A 为什么 List<B> 不能赋值给List<A>
假设有如下代码
class A{} class B extends A{}
List<B> b = new ArrayList<>();
List<A> a = b; // 编译报错
List<A> List<B> 在运行时泛型被抹除, 都是List类型,所以在运行时并无继承关系, A和B是声明List中存放的元素类型, List<> 尖括号中的泛型并不在类型继承体系中
编译报错的原因是,a指向b集合,a是List<A> 类型,意味这a.add(new A()) 这种完全是可以的, 因为LIst<A>可以接受 A类型, 但是a实际类型是 List<B> b.add(A()) 这样的操作是不可接受的,所以这样的操作编译不通过
问题2. 既然List<B>不能赋值给List<A> 那如何解决重复代码
假设有如下代码
class A {
public void func() {
}
}
class B extends A{
}
List<B> b = new ArrayList<>()
List<A> a = new ArrayList<>()
public void funcA(List<A> al) {
for (A a : al) {
a.func()
}
}
public void funcB(List<B> bl) {
for (B b : bl) {
b.func()
}
}
funcA(a)
funcB(b)
public void funcD(List<A> al) {
al.add(new B())
}
public void funcE(List<B> bl) {
bl.add(new B())
}
funcA和funcB都是List类型 都是调用A类中的func方法, 但是因为List<B> 不能赋值给List<A> 相同的逻辑却要写两遍,解决方案可以是用不带泛型的List接然后get元素之后强转成A,但是这个操作要被推迟到运行时
public void func(List l) {
for (Object o : l) {
((A)o).func();
}
}
同样 List<A>和List<B> 都可以进行add(new B())的操作,但是List<A> 不能向下强转成List<B> 相同的逻辑要写两边,解决方案可以是不带泛型的List接,然后调用add操作, 这样完全没问题,但同样也会被推迟到运行时
public void func(List l) {
l.add(new B());
}
这样的操作导致 为了解决泛型问题而忽略泛型!不优雅, 太不优雅了!
3. 怎么解决上面的问题
针对问题2中的相同代码问题,
funcA和funcB, List<A>中存储的都是A类型,List<B>中存储的都是B类型,而B类型一定是A类型, 所以List<B> 可以认为存储的都是A类型, 如果我们把List<B>中的B向上抽象一下, 表示成 List<? extends A> List<A>中的A也抽象一下,表示成List<? extends A>, 这样类型就一样了, 那么是不是就可以解决上面的问题
public void func(List<? extends A> l) {
for (A a : l) {
a.func()
}
}
funcC和funcD List<B>中可以存储B类型 List<A> 可以存储A类型, 可以存储A类型,那一定可以存储B类型,因为B继承于A, 既然funcC和funcD的代码都是要王List<A>和List<B>中放入B类型, B类型比A类型更具体, 那么是不是可以把 List<A> 向下具化成 List<? super B> 同样List<B>也具化成List<? super B> 这样类型就一样了,是不是就可以解决上面的问题
public viod func(List<? super B> l) {
l.add(new B())
}
相比之下,优雅,太优雅了!
4.把泛型向上抽象或者向下具化又带来了什么问题
把 List<B> 向上抽象 变成了 List<? extends A>
List<B> b = new ArrayList<>()
b.add(new B())
List<? extends A> a = b;
a.add(new B()) // 编译报错,向上抽象丢失了具体类型,所以当放一个具体类型时报错
原先List<B> b 在这个b对象上进行add(new B()) 的操作是完全没问题的,但是现在List<? extends A> 执行add(new B()) 编译不通过
原因也比较简单 泛型的B信息被替换成了? extends A 之前的B泛型丢了, List<? extends A> 唯一能确定的是我指向的集合里的元素都是A类型,但是具体是那个具体类型不知道,可能是List<C> 向上抽象转来的,也可能是List<B> 向上抽象来的, 那往List<? extends A> 这个集合里add(new B()) 我就没法确定他的正确性,万一是List<C>来的呢, 这不把B加到了List<C>里的吗
回过头我们看一下Collection中add 方法定义
boolean add(E e);
如果我们的集合是List<? extend A> 那么这里的E, IDE的提示是capture of ?extends A 类型, capture of ? extends A 这个表示编译器在计算表达式时,从代码中推断出来具体类型, 这就有意思了, 熟悉的java方法通常参数类型是给定的,比如A类型 B类型 但是这里的? 需要一个A或者A的子类型,具体是A的哪种子类型,编译器不知道, 可能是C,可能是D,也可能是A,所以除了null可以被传递外,其他值类型无法匹配,所以add方法基本不可用
再看看get方法定义
E get(int i);
如果我们的集合是List<? extend A> 那么这里的E, IDE的提示是返回一个capture of ?extends A 类型, 我们需要一个变量去接收这种类型, 由于都是A的子类, 所以用 A去接 A a = list.get(0) 没有问题,符合类型推导
把 List<A> 向下具化 变成了 List<? super B>
List<A> a = new ArrayList<>()
A a1 = a.get(0);
List<? super B> b = a;
A a1 = b.get(0); // 编译报错, 向下具化, 丢失了父类型, b的父类型时什么,编译器不知道,那肯定不能用A接,