用简单的英语解释协方差,不变性和相反性?


113

今天,我读了一些有关Java中协方差,协方差(和不变性)的文章。我阅读了英文和德文的Wikipedia文章,以及其他来自IBM的博客文章和文章。

但是我仍然对这些到底是什么感到困惑?有人说这与类型和子类型之间的关系有关,有人说与类型转换有关,有人说它用于确定方法是被重写还是被重载。

因此,我正在寻找一个简单的英语解释,它向初学者展示了协方差和逆方差(以及不变性)。加号是一个简单的例子。


请参阅此帖子,它可能对您有所帮助: stackoverflow.com/q/2501023/218717
Francisco Alvarado

3
也许更好的是程序员的堆栈交换类型问题。如果您确实在那儿发帖,请考虑说出您了解的内容以及具体使您感到困惑的内容,因为现在您正在要求某人为您重新编写整个教程。
气垫船充满鳗鱼,

Answers:


288

有人说这与类型和子类型之间的关系有关,有人说与类型转换有关,还有人说它用于确定方法是被覆盖还是被重载。

上述所有的。

从本质上讲,这些术语描述了类型转换如何影响子类型关系。也就是说,如果AB是类型,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协变,逆变还是不变?协变意味着a List<String>是的子类型List<Object>,相反,a 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。就像最后一个示例中的普通分配一样,“右侧”必须是“左侧”的子类型,即,仅当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))必须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() { ... }
}

我希望我覆盖了所有内容,或者更确切地说,是划伤了表面。我仍然希望它将有助于理解类型差异的抽象但重要的概念。


1
另外,由于Java 1.5的重载时允许使用自变量类型。我想你错过了。
布赖恩·戈登

13
是吗 我只是在eclipse中尝试过,编译器认为我打算重载而不是覆盖,并且在子类方法上放置@Override注释时拒绝了代码。您是否有证据表明Java支持协变参数类型?
Meriton

1
啊,你是对的。我相信有人自己检查一下。
布莱恩·戈登

1
我阅读了许多文档,并观看了有关此主题的一些讨论,但这是最好的解释。非常感谢。
minzchickenflavor

1
+1代表绝对的精明和简单A ≤ B。这种表示法使事情变得更加简单和有意义。好读书...
罗密欧·塞拉

12

以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时。矛盾表示您准备处理一个更笼统的论点。


8

方差是关于具有不同泛型参数的类之间的关系。他们之间的关系是我们可以抛弃他们的原因。

Co和Contra方差是很合逻辑的事情。语言类型系统迫使我们支持现实生活中的逻辑。通过示例很容易理解。

协方差

例如,您想购买一朵花,而您所在的城市有两家花店:玫瑰店和雏菊店。

如果您问某人“花店在哪里?” 有人告诉你玫瑰花店在哪里,可以吗?是的,因为玫瑰是一朵花,如果您想购买一朵花,您可以购买玫瑰。如果有人用菊花店的地址回答了您,则同样适用。这是协方差的示例:如果产生通用值(从函数返回),则可以强制转换A<C>A<B>,其中C是的子类。协方差与生产者有关。BA

类型:

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<B>A<C>,在那里C是子类B,如果A消耗的通用值。矛盾是关于消费者的。

类型:

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());

您可以在Source中找到更多信息


@Peter,谢谢,这很公平。不变就是在具有不同通用参数的类之间没有关系时,即,无论B和C之间的关系如何,都不能将A <B>强制转换为A <C>。
VadzimV
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.