Last year, I wrote a blog about my own understanding of covariance and contravariance. Looking back now, I found that it was still too "superficial" at the time, and I didn't understand it at all. Not long ago, I wrote a blog about "black" Java generics. When I turned around, it was "superficial". When I was learning Java generics today, I saw covariance and contravariance again. But it is still "superficial". Students who read this blog are welcome to leave a message to exchange.
What are covariance and contravariance?
What exactly are covariance and contravariance? Let's look at an example:
//Java
Object[] objects = new String[2];
//C#
object[] objects = new string[2];
This is covariance. Both C# and Java are supported 数组协变
languages. It seems that saying it means not saying it. Don't worry, take your time.
We all know that the String type in C# and Java is inherited from Object. Let’s just remember it String ≦ Object
, indicating that String is a subtype of Object, and the object of String can be assigned to the object of Object.
The array type Object[] of Object can be understood as a new type constructed from Object, which can be considered as a type 构造类型
, remember f(Object) (which can be compared to the definition of functions in junior high school mathematics), then we Covariance and contravariance can be described as follows:
- When A ≤ B, if there is f(A) ≤ f(B), then f is called
协变
; - When A ≤ B, if there is f(B) ≤ f(A), then f is called
逆变
; - If the above two relations do not hold, it is called
不可变
.
In fact, as the name implies, covariance and contravariance represent a type of transformation relationship: a relationship between relative "subtypes" between "constructed types". It's just that I (and probably everyone) are usually confused by some articles on the Internet. "Association" represents a natural conversion relationship, such as the above String[] ≦ Object[]
, this is what people often say when learning object-oriented programming languages:
Subclass variables can be assigned to parent class variables, but parent class variables cannot be assigned to subclass variables.
而“逆”则不那么直观,平时用的也很少,后面讲Java泛型中的协变和逆变
会看到例子。
不可变
的例子就很多了,比如Java中List< Object >
和List< String >
之间就是不可变的。
List<String> list1 = new ArrayList<String>();
List<Object> list2 = list1;
这两行代码在Java中肯定是编译不过的,反过来更不可能,C#中也是一样。
那么协变
和逆变
作用到底是什么呢?我个人肤浅的理解:主要是语言设计的一种考量,目的是为了增加语言的灵活性和能力。
里氏替换原则
再说下面内容之前,提下这个大家都知道的原则:
有使用父类型对象的地方都可以换成子类型对象。
假设有类Fruit和Apple,Apple ≦ Fruit,Fruit类有一个方法fun1,返回一个Object对象:
public Object fun1() {
return null;
}
Fruit f = new Fruit();
//...
//某地方用到了f对象
Object obj = f.fun1();
那么现在Aplle对象覆盖fun1,假设可以返回一个String对象:
@Override
public String fun1() {
return "";
}
Fruit f = new Apple();
//...
//某地方用到了f对象
Object obj = f.fun1();
那么任何使用Fruit对象的地方都能替换成Apple对象吗?显然是可以的。
举得例子是返回值,如果是方法参数呢?调用父类方法fun2(String)的地方肯定可以被一个能够接受更宽类型的方法替代:fun2(Object)......
返回值协变和参数逆变
上面提到的Java和C#语言都没有把函数作为一等公民,那么那些支持一等函数的语言,即把函数也看做一种类型是如何支持协变和逆变的以及里氏原则的呢?
也就是什么时候用一个函数g能够替代其他使用函数f的地方。答案是:
函数f可以安全替换函数g,如果与函数g相比,函数f接受更一般的参数类型,返回更特化的结果类型。《维基百科》
这就是是所谓的对输入类型是逆变的而对输出类型是协变的
Luca Cardelli提出的规则
虽然Java是面向对象的语言,但某种程度上它仍然遵守这个规则,见上一节的例子,这叫做返回值协变
,Java子类覆盖父类方法的时候能够返回一个“更窄”的子类型,所以说Java是一门可以支持返回值协变的语言。
类似参数逆变
是指子类覆盖父类方法时接受一个“更宽”的父类型。在Java和C#中这都被当作了方法重载
。
可能到这又绕糊涂了,返回值协变
和参数逆变
又是什么东东?回头看看协变和逆变的理解。把方法当成一等公民:
构造类型:Apple ≦ Fruit
返回值:String ≦ Object
参数:Object ≧ String
以上都是我个人对协变和逆变这两个概念的理解(欢迎拍砖)。说个题外话:“概念”是个很抽象的东西,之前听到一个不错说法,说概念这个单词英文叫做concept
,con
表示“共同的”,cept
表示“大脑”。
Java泛型中的协变和逆变
一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>
和List<String>
之间是不可变的。但当我们在Java泛型中引入通配符这个概念的时候,Java 其实是支持协变和逆变的。
看下面几行代码:
// 不可变
List<Fruit> fruits = new ArrayList<Apple>();// 编译不通过
// 协变
List<? extends Fruit> wildcardFruits = new ArrayList<Apple>();
// 协变->方法的返回值,对返回类型是协变的:Fruit->Apple
Fruit fruit = wildcardFruits.get(0);
// 不可变
List<Apple> apples = new ArrayList<Fruit>();// 编译不通过
// 逆变
List<? super Apple> wildcardApples = new ArrayList<Fruit>();
// 逆变->方法的参数,对输入类型是逆变的:Apple->Fruit
wildcardApples.add(new Apple());
可见在Java泛型中通过extends
关键字可以提供协变的泛型类型转换,通过supper
可以提供逆变的泛型类型转换。
关于Java泛型中supper
和extends
关键字的作用网上有很多文章,这里不再赘述。只举一个《Java Core》里面supper
使用的例子:下面的代码能够对实现Comparable
接口的对象数组求最小值。
public static <T extends Comparable<T>> T min(T[] a) {
if (a == null || a.length == 0) {
return null;
}
T t = a[0];
for (int i = 1; i < a.length; i++) {
if (t.compareTo(a[i]) > 0) {
t = a[i];
}
}
return t;
}
这段代码对Calendar
类是运行正常的,但对GregorianCalendar
类则无法编译通过:
Calendar[] calendars = new Calendar[2];
Calendar ret3 = CovariantAndContravariant.<Calendar> min(calendars);
GregorianCalendar[] calendars2 = new GregorianCalendar[2];
GregorianCalendar ret2 = CovariantAndContravariant.<GregorianCalendar> min(calendars2);//编译不通过
如果想工作正常需要将方法签名修改为: public static <T extends Comparable<? super T>> T min(T[] a)
至于原因,大家看下源码和网上大量关于supper
的作用应该就明白了,我这里希望能够给看了上面内容的同学提供另外一个思路......
结束语
C#虽然不支持泛型类型的协变和逆变(接口和委托是支持的,我之前的那篇博客也提到了),至于为什么C#不支持,《深入解析C#》中说是主要归结于两种语言泛型的实现不同:C#是运行时的,Java只是一个“编译时”特性。但究竟是为什么还是没说明白,希望有时间再研究下。