以java为例理解协变性
这篇文章以java为例,解释下语言的类型系统中的几个重要概念,协变性(covariance)、逆变性(contravariance)和无关性(invariant)
在面向对象语言中由于继承的存在,安全的类型转换对于书写正确的代码至关重要。而上面几个概念就是用来描述这种类型转换的性质的。
首先,看下这几个概念的定义:
如果M和N是类型,f()表示类型转换,≤表示子类型关系,(例如M≤N,表示M是N的子类)那么:
如果M≤N 则f(M) ≤f(N) 那么 f()是协变的
如果M≤N 则f(N) ≤f(M) 那么 f()是逆变的
如果上面两种都不成立,那么f()是无关的
举个例子,首先规定如下继承规则,后面例子中不另行说明都默认有这个定义 C≤B≤A
class A{}
class B extends A{}
class C extends B{}
然后,规定f(x)=x[]
B[] w2 = null;
A[] q1 = w2;
B[] q2 = w2;
C[] q3 = w2;//ERROR:编译错误Type mismatch
由上可得,B[]为A[]的子类,这样就证明数组变换具有协变性
这说明在java中,数组具有协变性的
再看个例子
class Animal{
public B deal(){return null;}
}
class Cat extends Animal{
@Override
public B deal(){return null;}
}
Cat继承了deal()函数,deal()的返回值为A则会发生编译错误,返回为B和C则正确。这说明继承覆盖函数返回值具有协变性。
那对于继承覆盖函数的参数呢?
class Animal
{
void deal(B t){}
}
class Cat extends Animal
{
@Override
void deal(B t){}
}
Cat的deal函数为A或者C都会报错,这说明继承覆盖函数的参数具有不变性。当然,去掉Override标志后,函数重载deal函数,参数写什么都无所谓,但是那样连类型转换都没有发生,就没有讨论的价值了。
几个基本的情况都清楚了,下面看下java的泛型List<?>
List<B> t1 = null;
List<B> t2 = null;
t1 = t2;
很明显,t2的类型为List<A>,List<C>都会报编译错误
这说明List<>和数组不同,具有不变性!
所幸,对于这种尴尬的情况,java提供了通配符这一功能,来解决这一问题,extends和super,前一个代表了协变性,后一个代表逆变性。
List<A>t1 = null;
List<B>t2 = null;
List<C>t3 = null;
List<?extends B> t = null;
t = t1;//ERROR
t = t2;
t = t3;
List<A>t1 = null;
List<B>t2 = null;
List<C>t3 = null;
List<?super B> t = null;
t = t1;
t = t2;
t = t3;//ERROR