协变、不变和逆变的解释(用通俗易懂的英语)?

165
今天我读了一些有关Java中协变、逆变(和不变性)的文章。我阅读了英文和德文维基百科文章,以及IBM的一些博客帖子和文章。
但是我仍然有点困惑这些到底是关于什么?有些人说这是关于类型和子类型之间的关系,有些人说这是关于类型转换,还有一些人说它用于决定方法是覆盖还是重载。
因此,我正在寻找一个简单易懂的英文解释,向初学者展示协变、逆变(和不变性)是什么。如果有简单的例子就更好了。

请参考此帖子,可能对您有帮助:https://dev59.com/VnE95IYBdhLWcg3wApHk - Francisco Alvarado
4
也许更适合在程序员交流平台上提问。如果你想在那里发布问题,请考虑明确表明你理解了什么,以及具体哪些部分让你感到困惑,因为现在你似乎要求别人重新为你编写整个教程。 - Hovercraft Full Of Eels
3个回答

360
有些人认为它与类型和子类型之间的关系有关,有些人说它是关于类型转换的,还有人说它用于确定方法是重写还是重载。
以上都是对的。
本质上,这些术语描述了子类型关系如何受类型转换的影响。也就是说,如果A和B是类型,f是一种类型转换,≤是子类型关系(即A≤B表示A是B的子类型),则我们有:
- 如果A≤B意味着f(A)≤f(B),则f是协变的。 - 如果A≤B意味着f(B)≤f(A),则f是逆变的。 - 如果以上两者均不成立,则f是不变的。
让我们考虑一个示例。让f(A)=List,其中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;

只有当y的类型小于等于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以来,覆盖时允许逆变参数类型。我想你没注意到这一点。 - Rag
13
它们是吗?我刚在Eclipse中尝试了一下,编译器认为我想要重载而不是覆盖,当我在子类方法上放置了@ Override注释时拒绝了代码。你有任何证据支持Java支持逆变参数类型的说法吗? - meriton
1
啊,你说得对。我没有自己检查就相信了别人的话。 - Rag
2
我阅读了很多关于这个主题的文档,并观看了一些讲座,但这绝对是最好的解释。非常感谢。 - minzchickenflavor
1
对于 A ≤ B 的简洁明了的表示法,我非常赞同并给予加分。这种表示法使得事情变得更加简单和有意义。阅读愉快... - Romeo Sierra
显示剩余7条评论

30

方差(Variance)是关于具有不同泛型参数的类之间的关系的。它们之间的关系是我们能够进行类型转换的原因。

协变(Covariance)和逆变(Contra variance)是非常合乎逻辑的事情。语言类型系统强制我们支持现实生活中的逻辑。通过示例很容易理解。

协变

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

如果您问某人“花店在哪里?”并且有人告诉您玫瑰花店在哪里,那么可以吗?是的,因为玫瑰是花,如果您想买花,您可以买一朵玫瑰花。如果有人回复您雏菊花店的地址,同样也可以。这是协变的例子:如果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>,其中 CB 的子类。 反变关注的是消费者。

类型:

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

您可以在中找到更多相关的IT技术内容。


@Peter,谢谢,这是一个公正的观点。不变性是指具有不同泛型参数的类之间没有关系,即无论B和C之间的关系如何,都不能将A<B>强制转换为A<C>。 - VadzimV

13

在处理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时。逆变表示您已准备好处理更一般的参数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接