java 协变与逆变

我们首先来看下面这两行代码:

Integer a = 1;
Number number = a;

so easy对吧?那我们再看这两行代码:

List<Integer> list1 = new ArrayList<Integer>();
List<Number> list = list1;

你认为第二行的list编译器会通过么?答案是不会的。要解答这个问题,我们就要聊到协变与逆变,泛型的通配符?和extends和super。
首先要想第二行的list编译通过需要对其进行改造,改造如下:

List<? extends Number> list2 = list1;  
or
List<? super Integer> list3 =list1;

list2与list3有什么区别呢?
list2只能获取数据不能添加数据,list3只能添加数据不能获取数据。
下面就有两个问题:
1为啥通过? extends或者? super就能解决list编译不通过的问题。
2为啥list2只能获取数据,list3只能添加数据。
先回顾一下知识:我们知道,java中的泛型是不可变的,它会在JVM编译时就进行类型擦除,当我们给你一个泛型属性或者集合添加数据时都是需要明确指定具体类型的。通配符?是用来放任意类型的,比如List<? extends Number> list2,list2可以放入Integer类型的数据,也可以放入Float类型的数据。
对于第一个问题,因为java泛型是不可变的,但是可以通过extends关键字可以提供协变的泛型类型转换,通过supper可以提供逆变的泛型类型转换。
对于第二个问题,我们跟源码进去看看,源码如下所示:

public interface List<E> extends Collection<E> {
	boolean add(E e);
}

我们可以看到List接口是泛型接口,泛型是不可变的,现在把List的泛型E变为了Number,所以编译器是不会通过的。同时我们也有个疑问,为啥list2只能获取数据,list3只能添加数据。
协变与逆变的定义:
假设有两个类型A和B,则他们各自的构造类型分别为f(A),f(B);
当A ≦ B时,如果有f(A) ≦ f(B),那么f叫做协变;
当A ≦ B时,如果有f(B) ≦ f(A),那么f叫做逆变;
我们也可以简单理解所谓协变就是A是B的子类型,逆变就是A是B的父类型。我们可以看到协变会导致范围缩小的,逆变会导致范围扩大的。
针对list2,此时可以看到接口List的泛型E变成了? extends Number,这个? extends Number 通配符告诉编译器我们在处理一个类型Number的子类型,但我们不知道这个子类型究竟是什么。因为没法确定,为了保证类型安全,java就不允许往里面添加任何数据。针对list3,此时可以看到接口List的泛型E变成了? super Integer,其表示list所持有的类型为Integer或者Integer的众多父类之一的类型,如果从list3中获取数据,我们就发现list3中的元素类型是不确定的,但是给list3添加元素都是确定的,都是Integer类型。从上面的例子可以看出,extends确定了泛型的上界,而super确定了泛型的下界。
扩展一下:

1jdk版本升级时引入协变。
在Java 1.4中,子类覆盖(override)父类方法时,形参与返回值的类型必须与父类保持一致:

class Super {
    Number method(Number n) { ... }
}

class Sub extends Super {
    @Override 
    Number method(Number n) { ... }
}

从Java 1.5开始,子类覆盖父类方法时允许协变返回更为具体的类型:

class Super {
    Number method(Number n) { ... }
}
class Sub extends Super {
    @Override 
    Integer method(Number n) { ... }
}

2设计原则之一:里氏替换原则。
设计模式中有六大设计原则。而里氏替换原则就是其中之一,感兴趣的朋友可以自行学习。
为啥要说到里氏替换原则呢?我们先看里氏替换原则最直白的定义:
只要有父类出现的地方,都可以用子类来替代。
上面我们操作泛型集合list时,可以看到存取的泛型类型分别是? extends Number和? super Integer,因为使用<? extends Number>后,如果泛型参数作为返回值,用T接收一定是安全的,也就是说使用这个函数的人可以知道你生产了什么东西;而使用<? super Integer>后,如果泛型参数作为入参,传递T及其子类一定是安全的,也就是说使用这个函数的人可以知道你需要什么东西来进行消费。
java.util.Collections的copy方法(JDK1.7)完美地诠释了PECS,源码如下:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

里氏替换原则(PECS)总结:
要从泛型类取数据时,用extends;
要往泛型类写数据时,用super;
既要取又要写,就不用通配符(即extends与super都不用)
参考博客:
https://www.cnblogs.com/keyi/p/6068921.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值