今天,我在Java中阅读了一些关于协方差,逆变(和不变性)的文章。我阅读了英文和德文维基百科的文章,以及IBM的一些其他博客文章和文章。
但我仍然对这些究竟是什么感到困惑?有人说它是关于类型和子类型之间的关系,有人说它是关于类型转换的,有些人说它用于决定方法是否被覆盖或重载。
所以我正在用简单的英语寻找一个简单的解释,它向初学者展示了协方差和逆变(和不变性)。加上一个简单的例子。
答案 0 :(得分:251)
有人说它是关于类型和子类型之间的关系,其他人说它是关于类型转换的,而其他人则说它用于决定方法是否被覆盖或过载。
以上所有。
从本质上讲,这些术语描述了子类型关系如何受到类型转换的影响。也就是说,如果A
和B
是类型,则f
是类型转换,并且≤子类型关系(即A ≤ B
表示A
是子类型B
),我们有
f
暗示A ≤ B
,则f(A) ≤ f(B)
是协变的
如果f
暗示A ≤ B
f(B) ≤ f(A)
是逆变的
f
如果以上都不存在让我们考虑一个例子。让f(A) = List<A>
通过
List
class List<T> { ... }
f
是协变的,逆变的还是不变的?协变意味着List<String>
是List<Object>
的子类型,逆变,List<Object>
是List<String>
的子类型,并且不变,它们都不是另一个的子类型,即{ {1}}和List<String>
是不可转换的类型。在Java中,后者是真的,我们说(有点非正式地)泛型是不变的。
另一个例子。让List<Object>
。 f(A) = A[]
是协变的,逆变的还是不变的?也就是说,String []是Object []的子类型,Object []是String []的子类型,还是既不是另一个的子类型? (答案:在Java中,数组是协变的)
这仍然相当抽象。为了使其更具体,让我们看看Java中的哪些操作是根据子类型关系定义的。最简单的例子是赋值。声明
f
仅在x = y;
时编译。也就是说,我们刚刚学会了这些陈述
typeof(y) ≤ typeof(x)
不会用Java编译,而是
ArrayList<String> strings = new ArrayList<Object>();
ArrayList<Object> objects = new ArrayList<String>();
意愿。
子类型关系重要的另一个例子是方法调用表达式:
Object[] objects = new String[1];
非正式地说,通过将result = method(a);
的值赋给方法的第一个参数,然后执行方法的主体,然后将方法返回值赋给a
来评估此语句。与上一个示例中的普通赋值一样,“右侧”必须是“左侧”的子类型,即此语句只有在result
和typeof(a) ≤ typeof(parameter(method))
时才有效。也就是说,如果方法是通过:
returntype(method) ≤ typeof(result)
以下表达式都不会编译:
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))
必须至少是协变的。
注意上面的“至少”。这些是最低要求,任何合理的静态类型安全面向对象编程语言都会强制执行,但编程语言可能会选择更严格。对于Java 1.4,当覆盖方法时,参数类型和方法返回类型必须相同(类型擦除除外),即覆盖时为f(A) = returntype(method asdeclaredin(A))
。从Java 1.5开始,在覆盖时允许使用协变返回类型,即以下内容将在Java 1.5中编译,而不是在Java 1.4中编译:
parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))
我希望我涵盖了所有内容 - 或者更确切地说,表面上划了一下。我仍然希望它有助于理解类型方差的抽象但重要的概念。
答案 1 :(得分:11)
采用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;
可以看出:
现在,与B是A的子类型有关,并且相反。可以通过更具体的知识引入以下更强的类型。在子类型中。
协方差(Java中提供)很有用,可以说在子类型中返回更具体的结果;尤其是当A = T且B = S时。 逆变论说你准备好处理一个更普遍的论点。
答案 2 :(得分:3)
方差是关于具有不同泛型参数的类之间的关系。他们之间的关系是我们可以抛弃他们的原因。
Co和Contra方差是很合逻辑的事情。语言类型系统迫使我们支持现实生活中的逻辑。通过示例很容易理解。
例如,您想购买一朵花,而您所在的城市有两家花店:玫瑰店和雏菊店。
如果您问某人“花店在哪里?”有人告诉你玫瑰花店在哪里,可以吗?是的,因为玫瑰是一朵花,所以如果您想买花,可以买玫瑰。如果有人用菊花店的地址答复您,则同样适用。
这是协方差的示例:您可以将A<C>
强制转换为A<B>
,其中C
是B
的子类,如果{{1 }}产生通用值(该函数的结果返回)。协方差与生产者有关。
类型:
A
问题是“花店在哪里?”,答案是“那里的花店”:
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();
}
}
例如,您想将花送给女友。如果您的女朋友爱上一朵花,您可以将她视为爱玫瑰的人,还是爱雏菊的人?是的,因为如果她喜欢任何花,她都会喜欢玫瑰和雏菊。
这是 convarivariance 的示例:您可以将static FlowerShop<? extends Flower> tellMeShopAddress() {
return new RoseShop();
}
强制转换为A<B>
,其中A<C>
是C
的子类,如果{ {1}}消耗通用值。矛盾是关于消费者的。
类型:
B
您正在考虑将爱花的女友当作爱玫瑰的人,并给她玫瑰:
A
您可以在Source上找到更多内容。