J2EE泛型中的协变与逆变

先举例说说covariance。用不太严谨的语言来举例:
·如果我们在整数集上能定义“<”(小于)关系,
·然后有一个函数f(x),定义域是整数集,值域我们可以不关心是什么,不过为了方便举例这里假设值域也是整数集

这样,如果在整数集上有
x < y
如果能保证
f(x) < f(y)
也成立,那么“f”作为一种变换,对“<”关系就是covariant的。

如果用“顺序”来想像,原本整数集可以按“<”关系排序,经过“f”的变换之后这个排序没有变化,这就是covariant的变化;如果排序关系反过来了,就是contravariant。

那么看类型系统的例子。假设有一种关系叫做“子类型”关系,记为“<:”。这个关系是自反和传递的,也就是说,
T <: T 成立
如果 T <: U, U <: V,那么 T <: V 也成立。

然后再看一种变换,叫“泛型”。泛型类型的定义域是类型,值域也是类型,也就是一种从类型到类型的变换。假如说记为“G[T]”。

这样,如果有
T <: U
此时如果 G[T] <: G[U],那么G这种变换就是covariant的;
反之,如果 G[U] <: G[T],那么这种变化就是contravariant的。
而在Java的泛型设计中,G[T]与G[U]不构成任何<:关系,所以说G与“子类型关系”是invariant(不变)的。

===========================================================

这里所说的declaration-site就是“类型本身的声明的地方”。举例说的话,
Java代码 复制代码  收藏代码
  1. public class String { ... }  
public class String { ... }

这里就是String类的declaration-site。或者可以翻译为“声明点”。


Java代码 复制代码  收藏代码
  1. String s = ...;  
String s = ...;

这里就是String类型的一个use-site。或者可以翻译为“使用点”“使用地点”之类。

===========================================================

接下来看看use-site variance与declaration-site variance。
这两种都是表达variance(co-或contra-variance)的方式。

先看declaration-site variance,因为实际上这种更直观一些。
在C# 4里,泛型接口上的泛型参数可以声明为covariant或者contravariant的。=> 参考资料
covariant的泛型参数用out关键字来修饰,contravariant的泛型参数用in关键字来修饰。

C#代码 复制代码  收藏代码
  1. public interface IEnumerable<out T> { ... }  

例如说这样,就声明了一个接收一个covariant的泛型参数的、名为IEnumerable的接口。variance直接在“类型声明”的地方就清楚的写明了。

这样,下面的赋值就能够成立:
C#代码 复制代码  收藏代码
  1. IEnumerable<string> strIter = ...;   
  2. IEnumerable<object> objIter = strIter;  

因为IEnumerable<string>是IEnumerable<object>的子类型,所以赋值匹配。

Java里的Iterable接口跟这个IEnumerable是对应的。但下面这段代码就无法成立,因为Java的泛型对子类型关系是invariant的:
Java代码 复制代码  收藏代码
  1. Iterable<String> strIter = ...;   
  2. Iterable<Object> objIter = strIter;  
Iterable<String> strIter = ...;
Iterable<Object> objIter = strIter;


===========================================================

然后看use-site variance。上面的Java代码如何改改就能成立呢?
Java代码 复制代码  收藏代码
  1. Iterable<? extends Object> objIter = strIter;  
Iterable<? extends Object> objIter = strIter;

就行了。为什么?因为在Java的泛型设计里,? extends T用于表达covariance。
<? extends Object>说的是,实际赋值过来的那个类型是Object的任意子类型都可以匹配。所以X<String>是X<? extends Object>的子类型,X<Object>也是X<? extends Object>的子类型。
我们只能在use-site使用这种记法而无法在Iterable类型的声明上使用,所以把这种记法叫做use-site variance。

于是,Java里泛型类型声明的地方虽然无法指定variance,但实际使用这些泛型类型时却可以任意指定所需要的variance,看起来是不是比declaration-site variance要更灵活?
这里太灵活正是很多不好理解的东西的来源。

===========================================================

继续讨论Java泛型与variance之前,先看看Java(与C#)的数组。Java的数组是covariant的,而且不是静态安全的,必须带有一些额外的运行时检查才可以保持类型系统的安全。
因为covariant,所以下面的赋值可以成立:
Java代码 复制代码  收藏代码
  1. String[] strArr = new String[10];   
  2. Object[] objArr = strArr;  
String[] strArr = new String[10];
Object[] objArr = strArr;

这里看起来都很正常。

当我们要从数组里取出东西的时候,
Java代码 复制代码  收藏代码
  1. Object o = objArr[1];  
Object o = objArr[1];

这样也没问题,类型都是匹配的。

但当我们要往数组里放入元素的时候,事情就不一定这么明显了:
Java代码 复制代码  收藏代码
  1. objArr[1] = new Fruit();  
objArr[1] = new Fruit();

objArr引用的是一个String[]的实例,它只能装String类型的引用,不能装别的。
但Java源码被静态编译的时候,语言规范没有要求编译器发现objArr实际上引用的是String[]类型的实例;编译器只需要知道objArr的静态类型是Object[]。
objArr[1]的静态类型是Object,那么任意Object的子类型自然都应该能赋值匹配。所以这句赋值能通过编译。
然后到运行时数组的赋值会做子类型检查,发现Fruit不是String的子类型,然后抛出java.lang.ArrayStoreException。

===========================================================

回到泛型与variance。

在C# 4里,covariant的泛型参数只能出现在方法的返回值位置上,而不能出现在参数位置上。
前面举的IEnumerable接口的例子,里面有一个方法,挑出跟这例子相关的来看:
C#代码 复制代码  收藏代码
  1. public interface IEnumerable<out T> {   
  2.     IEnumerator<T> GetEnumerator();   
  3. }  

可以看到T出现在了GetEnumerator()方法的返回值的类型里。这个符合C# 4的规范。
但如果我们再加一个方法到该接口里,void Foo(T arg),就通不过编译。

C# 4里的 IList<T> 接口上的T就既不是covariant也不是contravariant的,因为它既要出现在参数位置上也要出现在返回值位置上。

类似的,contravariant的泛型参数只能出现在方法的参数位置上,而不能出现在返回值位置上。

从上面数组的covariance的情况,不难理解为什么需要做这样的限制:如果covariant泛型参数出现在了参数位置上,那就难以通过静态类型检查来保证类型安全,而必须添加运行时子类型检查。

===========================================================

Java的情况。List<? extends Fruit>类型的变量能够引用任意的Fruit类型的子类型的List<E>,例如说List<Fruit>、List<Apple>、List<Banana>之类。
如果有:
Java代码 复制代码  收藏代码
  1. List<Apple> apples = new ArrayList<Apple>();   
  2. apples.add(new Apple());   
  3. List<? extends Fruit> fruits = apples;  
List<Apple> apples = new ArrayList<Apple>();
apples.add(new Apple());
List<? extends Fruit> fruits = apples;

那么跟数组的情况相似,我们从fruits里拿出东西的时候肯定没问题,肯定是Fruit的子类型。
Java代码 复制代码  收藏代码
  1. Fruit f = fruits.get(0);  
Fruit f = fruits.get(0);


但如果要往fruits里放入东西,就开始有问题了:add(E e)方法的E,这里应该被替换为? extends Fruit。但这不是一个确定的类型;它用于表达“非确定的任意Fruit的子类型”,于是任意确定的Fruit的子类型反而无法跟它赋值匹配。
可以想像,你传入一个Banana实例,然后E说“我这个时候想要Apple”(任意Fruit的子类型);你换成传入Apple的实例,它又说“我现在想要Orange”了 这样的捉迷藏伤不起吧?

如果我们写:
Java代码 复制代码  收藏代码
  1. fruits.add(new Banana());  
fruits.add(new Banana());

Java该如何知道这个操作到底安不安全呢?很简单,它干脆就不让这段代码编译,这样就保证安全了。

实际上,Java语言的规定让covariant的泛型参数只能出现在返回值位置上才允许调用;上面add(E e)方法在参数位置上出现了泛型参数E,当E被替换为? extends X的时候,这个方法就不允许调用了。
类似的,contravariant泛型参数只能出现在参数位置上才允许调用。

PECS口诀是producer- extends, consumer- super。producer是“生产者”,自然应该“返回”某些东西;consumer是“消费者”,自然应该“接收”某些东西。
结合前面的例子,这个口诀应该能让人很容易记住extends是用来表示covariance的,而super是用来表示contravariance的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值