【问题标题】:Covariance, Invariance and Contravariance explained in plain English?用简单的英语解释协变、不变和逆变?
【发布时间】:2012-01-18 20:37:23
【问题描述】:

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

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

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

【问题讨论】:

  • 请参考这篇文章,可能对你有帮助:stackoverflow.com/q/2501023/218717
  • 也许更好的是程序员的堆栈交换类型问题。如果您确实在那里发帖,请考虑仅说明您所理解的内容,以及让您特别困惑的内容,因为现在您正在要求某人为您重新编写整个教程。

标签: java covariance contravariance


【解决方案1】:

有人说它是关于类型和子类型之间的关系,有人说它是关于类型转换,也有人说它用于决定一个方法是否被覆盖或重载。

以上所有。

本质上,这些术语描述了子类型关系如何受到类型转换的影响。即如果AB是类型,f是类型转换,≤子类型关系(即A ≤ B表示AB的子类型),我们有

  • f 是协变的,如果 A ≤ B 暗示 f(A) ≤ f(B)
  • 如果A ≤ B 暗示f(B) ≤ f(A),则f 是逆变的
  • 如果以上都不成立,f 是不变的

让我们考虑一个例子。让f(A) = List<A> 其中List 声明为

class List<T> { ... } 

f 是协变的、逆变的还是不变的?协变意味着List&lt;String&gt;List&lt;Object&gt; 的子类型,逆变意味着List&lt;Object&gt;List&lt;String&gt; 的子类型,并且不变量意味着两者都不是另一个的子类型,即List&lt;String&gt;List&lt;Object&gt; 是不可转换的类型。在 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() { ... }
}

我希望我涵盖了所有内容 - 或者更确切地说,只是触及了表面。我仍然希望它有助于理解类型变化这个抽象但重要的概念。

【讨论】:

  • 另外,因为 Java 1.5 逆变参数类型在覆盖时是允许的。我想你错过了。
  • 是吗?我只是在eclipse中尝试过,编译器认为我的意思是重载而不是覆盖,并在我在子类方法上放置@Override注解时拒绝了代码。您是否有任何证据证明 Java 支持逆变参数类型?
  • 啊,你是对的。我没有亲自检查就相信了某人。
  • 我阅读了很多文档并观看了一些关于这个主题的讨论,但这是迄今为止最好的解释。非常感谢。
  • +1 因为A ≤ B 绝对简洁和简单。这种符号使事情变得更加简单和有意义。好读...
【解决方案2】:

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

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

协方差

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

如果你问某人“花店在哪里?”有人告诉你玫瑰店在哪里,可以吗?是的,因为玫瑰是一朵花,如果你想买一朵花,你可以买一朵玫瑰。如果有人用雏菊店的地址回复您,这同样适用。 这是协方差的示例:您可以将A&lt;C&gt; 转换为A&lt;B&gt;,其中CB 的子类,如果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();
    }
}

问题是“花店在哪里?”,答案是“玫瑰店那里”:

static FlowerShop<? extends Flower> tellMeShopAddress() {
    return new RoseShop();
}

逆变

例如你想送花给你的女朋友。如果你的女朋友喜欢任何一朵花,你能认为她是一个喜欢玫瑰的人,还是一个喜欢雏菊的人?是的,因为如果她喜欢任何一朵花,她就会同时喜欢玫瑰和雏菊。 这是逆变的一个例子:你可以将A&lt;B&gt; 转换为A&lt;C&gt;,其中CB 的子类,如果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 强制转换为 A
【解决方案3】:

取java类型系统,然后是classes:

任何类型 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 时。 逆变表示您准备好处理更一般的论点。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-06
    • 1970-01-01
    • 1970-01-01
    • 2016-11-03
    • 2015-02-09
    • 2012-04-06
    相关资源
    最近更新 更多