协变和逆变以及泛型
什么是泛型,泛型中协变和逆变又是什么?
<? extends Number>是什么意思, 为什么不能给list里面add Integer 和Float对象
Number num = new Integer(1) ; //编译正确
ArrayList list = new ArrayList(); //编译错误
为什么同样的 Integer对象能被Number实例化, 而 ArrayList不可以被ArrayList实例化。
List<? extends Number> list = new ArrayList();
list.add(new Integer(1)); //编译错误
list.add(new Float(1.2f));//编译错误
涉及知识
在介绍泛型之前,我们先了解一下继承多态中引用的用法,和LSP(里氏替换原则)
引用相关
父类引用指向子类对象,反之不行(因为子类中有的属性方法,父类没有。基本原则[根据范围]:小(父类) = 大(子类))。
引用类型决定了能进行哪些操作。实际执行的是指向对象的内容
假设A extends B
A a = new B a(); //TODO
//>> TODO 形参数的赋值相当于 B b (形参) = A(实参) ; 也相当于 B b = new A();
//>> TODO 返回值的赋值相当于 a(形参) = b(返回值实参) ; 也相当于 A b = new B();
a = test(a);
public B test (B b ){
return b ;
}
LSP(Liskov Substitution Principle)里氏替换原则
子类对象(object of subtype/ derived class)能够替换程序(program)中父类对象(object of base /parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏
假设有类Fruit和Apple,Apple ≦ Fruit,Fruit类有一个方法fun1,返回一个Object对象:
public Fruit{
public Object fun1() {
return null;
}
public String fun2(String data){
return data;
}
public String fun2(Object data){
return data;
}
}
public static void main(String[] arg){
Fruit f = new Fruit();
//某地方用到了f对象
Object obj = f.fun1();
}
那么现在Aplle对象覆盖fun1,假设可以返回一个String对象:
public Aplle extends Fruit{
@Override
public String fun1() {
return "";
}
}
public static void main(String [] arg){
Fruit f = new Apple();
//...
//某地方用到了f对象
Object obj = f.fun1();
f.fun2(new Object());
}
那么任何使用Fruit对象的地方都能替换成Apple对象吗?显然是可以的。
泛型
泛型的英文名叫做generics,一系列和泛型相关的名词都是以generic为前缀
在类型中定义泛型,即Generic Types。类型可以是类,也可以是接口
//TODO 定义泛型,就是把需要的类型定义到类后面的尖括号里面,然后在类里面就可以把定义好的泛型像符号一样使用。多个泛型之间用逗号隔开
public class MyGenericClass<First,Second>{
private First first;
private Second second;
public MyGenericClass(First first,Second second){
this.first = first;
this.second = second;
}
}
在方法中定义泛型,即Generic Methods
public class DefineBoundedGenericTypesAppMain{
public <Another> Another getAnother(Another val){
return val;
}
}
//TODO 调用
DefineBoundedGenericTypesAppMain appMain = new DefineBoundedGenericTypesAppMain();
Parent another = appMain.<Parent> getAnother(new Parent());
泛型的作用
java并没有把泛型的具体类型记录在对象里面,只是在编译的时候做了一些检查,在使用的时候,做了强制类型转换(类型擦除就是把泛型信息删除了,然后变成根据泛型类型信息,加一个强制类型转换操作)
有界泛型
泛型类型不可以调用方法,因为不知道什么类型。如果需要使用某个类的方法,则需要给定类型的范围。
有界泛型解决的问题:让自己类的代码可以调用泛型类型的方法
代码如下:
public class GrandParent{
private Integer num;
public Integer getNum (){
return num;
}
}
//在泛型定义的这块如果知道这个泛型MyType继承自GrandParent这个类 就可以用下面这种方式定义
public class MygenericClass<MyType extends GrandParent>{
//TODO 然后这个引用就指向了GrandParent类
private MyType mytype;
public MygenericClass(MyType myType){
//TODO 所以在这里可以通过这个引用,调用GrandParent的方法
myType.getNem();
this.mytype = myType;
}
}
协变和逆变
定义
协变和逆变用来描述类型转换(type transformation)后的继承关系。那到底什么是协变和逆变? 先看例子:
Object [] object = new String[2];
这就是协变。
我们都知道Java中的String类型都是继承自Object,姑且记做String ≤ Object,表示String是Object的子类型,String对象可以赋值给Object的对象。左边是实例右边是引用
定义: 如果A、B表示类型, f(.) 表示类型转换,≤ 表示继承关系(比如,A ≤ B 表示A是由B派生出来的子类 )
- f(.)是协变(covariant)的, 当A ≤ B 时,有f(A) ≤ f(B)成立 即 B b = new A();
- f(.)是逆变(contravariant) 的, 当A ≤ B 时,有f(B) ≤ f(A)成立 即 A a = new B();
- f(.)是不变(invariant)的, 当A ≤ B 时,上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系
其实顾名思义,协变和逆变表示的一种类型转换的关系。构造类型
之间相当于子类型
之间的一种关系。[比如Object数组类型Object[] ,我们可以理解成由Object构造出来的一种新的类型,可以认为是一种构造类型]。
协
表示一种自然而然的转换关系,比如String[] ≤ Object[] 。这就是继承中所说的:子类变量能赋值给父类变量,父类变量不能赋值给子类变量
返回值协变和参数逆变
JAVA和C#语言都没有把函数作为一等公民,那么那些支持一等函数的语言,即吧函数也看成一种类型的是如何支持协变和逆变 以及里式原则的呢?
换句话说,就是什么时候用一个函数g能够替代其他使用函数f的地方。答案是:
函数f可以安全替换函数g,如果与函数g相比,函数f接受更一般的参数类型,返回更特化的结果类型。《维基百科》
这就是所谓的对输入类型是逆变的,而对输出类型是协变的
虽然Java是面向对象的语言,但某种程度上它仍然遵守这个规则。
返回值协变:Java子类覆盖父类方法的时候能够返回一个“更窄”的子类型。在Java中这被当做了方法重写
参数逆变 :类似参数逆变是指子类覆盖父类方法时接受一个”更宽“的父类型,在Java和C#中这都被当做了方法重载
回头看看开头LSP的代码,把方法当做一等公民:
构造类型 :Apple ≤ Fruit
返回值: String ≤ Object
参数: Object ≧ String ---->参数中实例是调用方法实参,引用是方法中的形参
不可变的例子
比如JAVA中的List 和 List之间就是不可变的
List<String> list1 = new ArrayList<String>();
List<Object> list2 = list1;
java中协变和逆变
Java泛型是不支持协变和逆变的,比如List和List之间是不可变的。但是在Java泛型中引入通配符的时候,Java其实是支持协变和逆变的。
Java泛型对协变和逆变的支持是为了支持范围更广的参数类型。
协变和逆变是针对引用类型而言的,可以用在返回值类型,参数类型,等引用类型上。创建对象的时候,不能使用协变和逆变
泛型中的通配符
1、 实现泛型的协变和逆变
Java中泛型是不变的,可有时需要实现协变和逆变,怎么办呢? 这时,通配符 **?**就派上了用场
- <? extends>实现了泛型的协变
List<? extends Number> list = new ArrayList<Integer>();
- <? super>实现了泛型的逆变
List<? super Number> list = new ArrayList<Object>();
代码举例
// 不可变
List<Fruit> fruits = new ArrayList<Apple>();// 编译不通过
// 协变 Apple ≤ Fruit
List<? extends Fruit> wildcardFruits = new ArrayList<Apple>();
// 协变->方法的返回值,对返回类型是协变的:Apple ≤ Fruit
Fruit fruit = wildcardFruits.get(0);
// 不可变
List<Apple> apples = new ArrayList<Fruit>();// 编译不通过
// 逆变 Fruit ≧ Apple
List<? super Apple> wildcardApples = new ArrayList<Fruit>();
// 逆变->方法的参数,对输入类型是逆变的:Fruit ≧ Apple
wildcardApples.add(new Apple());
<T extends Comparable<T>>
和<T extends Comparable<? super T>>
含义
<T extends Comparable<T>>
表明T
实现了Comaprable<T>
接口,此条件强制约束,泛型对象必须直接实现Comparable<T>
(所谓直接就是指不能通过继承或其他方式)<T extends Comparable<? super T>>
表明T
的任意一个父类实现了Comparable<? super T>
接口,其中? super T
表示?
泛型类型是T
的父类(当然包含T
),因此包含上面的限制条件,且此集合包含的范围更广
比如我们会在源码中会看到这种泛型的理解
特殊代码的理解
public static <T extends Comparable<? super T>> void sort (List<T> list)
- 首先:
public static void sort(List list)
- 为了安全性加泛型:
public static <T> void sort(List<T> list)
- 想要排序先比较,要有可比较性,因此T必须是Comparable的子类:
public static <T extends Comparable> void sort(List<T> list)
- Comparable接口也有泛型:
public static <T extends Comparable<T>> void sort(List<T> list)
- T的父类也行,<? super T>表示Comparable<>中的类型下限为T:
public static <T extends Comparable<? super T>> void sort (List<T> list)
下面代码运行成功
import java.util.GregorianCalendar;
class Demo<T extends Comparable<? super T>>{}
public class Test1
{
public static void main(String[] args) {
Demo<GregorianCalendar> p = null; // 编译正确
}
}
这个可以理解为<GregorianCalendar extends Comparable<Calendar>>
是可以运行成功的!因为Calendar
为GregorianCalendar
的父类并且实现了Comparable<Calendar>
,可查看api!.
下面代码运行失败
import java.util.GregorianCalendar;
class Demo<T extends Comparable<T>>{}
//这里把? super去掉了
public class Test
{
public static void main(String[] args) {
Demo<GregorianCalendar> p = null;
}
}
因为<T extends Comparable<T>>
相当于<GregorianCalendar extends Comparable<GregorianCalendar>>
但是GregorianCalendar
并没有实现Comparable<GregorianCalendar>
而是实现的Comparable<Calendar>
,这里不在限制范围之内所以会报错
为什么协变适用于读取,而逆变适用于写入
List<? extends Number> list = new ArrayList<Number>();
list.add(new Integer(1)); //error
list.add(new Float(1.2f)); //error
为什么上述代码,通过协变的list 在add 的时候会编译错误?
首先我们看下add的实现
public interface List<E> extends Collection<E> {
boolean add(E e);
}
在调用add的时候,泛型E自动变成了<? extends Number>,其表示list所持有的类型为,在Number与Number派生子类中的某一类型,其中包含Integer类型却又不特指为Integer类型,下限不确定,只确定了上限(如果list持有类型是Integer的子类,那add Integer的时候相当于 子类引用= 父类实例 会报错),所以java在这方面直接一刀切,通过协变出来的list,不可以add,所以发生编译错误。所以协变不适合写入。
而协变读取元素的时候,里面有丰富的类型信息,也就是说读取出来的是某个类型(T)的或者其子类,这样读出来才能才能放心大胆的把它当做T使用(因为子类可以当做父类来使用),所以协变适合读取
List<? super Number> list = new ArrayList<Object>();
list.add(new Integer(1));
list.add(new Float(1.2f));
而上述逆变的代码,直接确定了list持有的类型为Number或者Number的父类,下限确定之后,add Integer的时候相当于 父类引用= 子类实例所以逆变适合写入
当逆变读取元素的时候,里面只有T的父类的信息,而父类的引用只能读取父类的信息,而你添加的元素大部分是子类的。父类的引用不能调用子类的元素,局限性大,所以逆变不适合读取
总结
如果你想从一个数据类型里获取数据,使用 ? extends 通配符
如果你想把对象写入一个数据结构里,使用 ? super 通配符
如果你既想存,又想取,那就别用通配符。
参考链接
https://www.cnblogs.com/en-heng/p/5041124.html
https://www.zybuluo.com/zhanjindong/note/34147
https://blog.csdn.net/superman_xxx/article/details/69486437?utm_source=blogxgwz4
https://www.cnblogs.com/09120912zhang/p/8319899.html