今天,我读了一些关于Java中的协方差、逆变性(和不变性)的文章。我阅读了维基百科的英文和德文文章,以及IBM的其他一些博客文章和文章。
但是我仍然有点困惑,这些到底是关于什么?有人说它是关于类型和子类型之间的关系,有人说它是关于类型转换的,还有人说它是用来决定一个方法是被重写还是被重载的。
所以我在寻找一个简单的英语解释,向初学者展示协方差和逆变性(和不变性)是什么。这是一个简单的例子。
有人说它是关于类型和子类型之间的关系,有人说它是关于类型转换的,还有人说它是用来决定一个方法是被覆盖还是被重载的。
以上所有
本质上,这些术语描述了子类型关系如何受到类型转换的影响。即,若A
和B
为类型,f
为类型变换,且≤子类型关系(即A ≤ B
表示A
是B
的子类型),则有
- 如果
-
f
为协变如果A ≤ B
暗示f(B) ≤ f(A)
,则 -
f
为逆变 -
f
是不变的,如果以上两个都不成立
A ≤ B
暗示f(A) ≤ f(B)
,则让我们考虑一个例子。设f(A) = List<A>
,其中List
由
class List<T> { ... }
f
是协变、逆变还是不变?协变意味着List<String>
是List<Object>
的子类型,逆变意味着List<Object>
是List<String>
的子类型,不变意味着两者都不是对方的子类型,即List<String>
和List<Object>
是不可转换的类型。在Java中,后者是正确的,我们(有些非正式地)说泛型是不变的。
另一个例子。设f(A) = A[]
。f
是协变、逆变还是不变?也就是说,String[]是Object[]的子类型,Object[]是String[]的子类型,还是两者都不是另一个的子类型?(答案:在Java中,数组是协变的)
这仍然相当抽象。为了使其更具体,让我们看看Java中哪些操作是根据子类型关系定义的。最简单的例子是赋值。语句
x = y;
只有在typeof(y) ≤ typeof(x)
。也就是说,我们刚刚得知语句
ArrayList<String> strings = new ArrayList<Object>();
ArrayList<Object> objects = new ArrayList<String>();
不能在Java中编译,但是
Object[] objects = new String[1];
。
子类型关系很重要的另一个例子是方法调用表达式:result = method(a);
非正式地说,该语句的求值方式是将a
的值赋给方法的第一个参数,然后执行方法体,然后将方法的返回值赋给result
。与上一个示例中的普通赋值一样,"right hand side"必须是"left hand side"的子类型,即该语句只有在typeof(a) ≤ typeof(parameter(method))
和returntype(method) ≤ typeof(result)
时才有效。也就是说,if方法声明为:
Number[] method(ArrayList<Number> list) { ... }
下列表达式都不能编译:
Integer[] result = method(new ArrayList<Integer>());
Number[] result = method(new ArrayList<Integer>());
Object[] result = method(new ArrayList<Object>());
,
Number[] result = method(new ArrayList<Number>());
Object[] result = method(new ArrayList<Number>());
。
另一个与子类型有关的例子是重写。考虑:Super sup = new Sub();
Number n = sup.method(1);
,
class Super {
Number method(Number n) { ... }
}
class Sub extends Super {
@Override
Number method(Number n);
}
运行时将非正式地将其重写为:
class Super {
Number method(Number n) {
if (this instanceof Sub) {
return ((Sub) this).method(n); // *
} else {
...
}
}
}
对于要编译的标记行,重写方法的方法参数必须是被重写方法的方法参数的超类型,返回类型必须是被重写方法的子类型。形式上来说,f(A) = parametertype(method asdeclaredin(A))
必须至少是逆变的,而f(A) = returntype(method asdeclaredin(A))
必须至少是协变的。
注意上面的"至少"。这些是任何合理的静态类型安全的面向对象编程语言都会强制执行的最低要求,但编程语言可能会选择更严格的要求。在Java 1.4的情况下,当重写方法时,参数类型和方法返回类型必须相同(除了类型擦除),即重写时使用parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))
。从Java 1.5开始,在重写时允许协变返回类型,即以下代码将在Java 1.5中编译,但在Java 1.4中不能编译:
class Collection {
Iterator iterator() { ... }
}
class List extends Collection {
@Override
ListIterator iterator() { ... }
}
我希望我涵盖了所有内容——或者更确切地说,触及了表面。我仍然希望这将有助于理解抽象但重要的类型差异概念。
方差是关于具有不同泛型参数的类之间的关系。他们之间的关系是我们可以让他们出演的原因。
Co和Contra方差是非常合乎逻辑的东西。语言类型系统迫使我们支持现实生活中的逻辑。举例来说很容易理解。
协方差举个例子,你想买一朵花,而你所在的城市有两家花店:玫瑰花店和雏菊花店。
如果你问别人"花店在哪里?"如果有人告诉你玫瑰店在哪里,可以吗?是的,因为玫瑰是花,如果你想买花,你可以买玫瑰。如果有人回复你菊花店的地址,同样适用。这是协方差的例子:如果A
产生泛型值(作为函数的结果返回),则允许将A<C>
转换为A<B>
,其中C
是B
的子类。协方差是关于生产者的。
类型:
class Flower { }
class Rose extends Flower { }
class Daisy extends Flower { }
interface FlowerShop<T extends Flower> {
T getFlower();
}
class RoseShop implements FlowerShop<Rose> {
@Override
public Rose getFlower() {
return new Rose();
}
}
class DaisyShop implements FlowerShop<Daisy> {
@Override
public Daisy getFlower() {
return new Daisy();
}
}
问题是"花店在哪里",答案是"玫瑰店在那里";
static FlowerShop<? extends Flower> tellMeShopAddress() {
return new RoseShop();
}
逆变性
举个例子,你想送花给你的女朋友。如果你的女朋友喜欢任何一种花,你能认为她是一个喜欢玫瑰的人,还是一个喜欢雏菊的人?是的,因为如果她爱任何一种花,她会同时爱玫瑰和雏菊。这是一个逆变的例子:如果A
使用泛型值,则允许将A<B>
转换为A<C>
,其中C
是B
的子类。逆变是关于消费者的。
类型:
interface PrettyGirl<TFavouriteFlower extends Flower> {
void takeGift(TFavouriteFlower flower);
}
class AnyFlowerLover implements PrettyGirl<Flower> {
@Override
public void takeGift(Flower flower) {
System.out.println("I like all flowers!");
}
}
你把你爱花的女朋友当成爱玫瑰的人,送给她一朵玫瑰:
PrettyGirl<? super Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());
您可以在源代码中找到更多信息。
取java类型系统,然后分类:
任何类型T的对象都可以被T的子类型的对象代替
类型差异-类方法有以下结果
class A {
public S f(U u) { ... }
}
class B extends A {
@Override
public T f(V v) { ... }
}
B b = new B();
t = b.f(v);
A a = ...; // Might have type B
s = a.f(u); // and then do V v = u;
可以看出,
- T必须是S子类型(协变,因为B是A的子类型)。
- V必须是U的超类型(逆变,作为反向继承方向)。
现在,共-和反-与B是a的亚型相关,下面的强类型可以通过更具体的知识来介绍。
协方差(在Java中可用)是有用的,说明在子类型中返回更具体的结果;特别是当A=T和B=S时。逆变性表示您准备处理更一般的参数