问题

今天,我在Java中阅读了一些关于Covariance,Contravariance(和Invariance)的文章。我阅读了英文和德文维基百科的文章,以及IBM的一些其他博客文章和文章。

但我对这些究竟是什么仍然有点困惑?有人说它是关于类型和子类型之间的关系,有些人说它是关于类型转换的,有些人说它用于决定方法是否被覆盖或过载。

所以我正在用简单的英语寻找一个简单的解释,它向初学者展示了Covariance和Contravariance(和Invariance)。加上一点简单的例子。


#1 热门回答(223 赞)

有人说这是关于类型和子类型之间的关系,其他人说它是关于类型转换的,而其他人说它用于决定方法是否被覆盖或超载。

上述所有的。

从本质上讲,这些术语描述了子类型关系如何受到类型转换的影响。也就是说,ifAB类型,f是类型转换,并且≤子类型关系(即5223666957,即332329976是子类型B),我们有

  • 如果A≤B意味着f(A)≤f(B),则f是协变的
  • 如果A≤B意味着f(B)≤f(A),则f是逆变的
  • 如果上述两者都不成立,则f是不变的

我们来看一个例子吧。 Letf(A) = List<A>whereList由声明

class List<T> { ... }

Isf是否变形,逆变或不变?协变意味着aList<String>List<Object>的子类型,逆变量aList<Object>List<String>的子类型,并且不变量,它们都不是另一个的子类型,即.List<String>List<Object>是不可转换的类型。在Java中,后者是真的,我们说(有点非正式地),本体是不变的。

另一个例子。 Letf(A) = A[]。 Isf是变形的,逆变的还是不变的?也就是说,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,当覆盖方法时,参数类型和方法返回类型必须相同(类型擦除除外),即覆盖时的参数类型和方法返回类型。从Java 1.5开始,在覆盖时允许使用协变返回类型,即以下内容将在Java 1.5中编译,但在Java 1.4中不编译:

class Collection {
    Iterator iterator() { ... }
}

class List extends Collection {
    @Override 
    ListIterator iterator() { ... }
}

我希望我涵盖了所有内容 - 或者更确切地说,表面上划了一下。我仍然希望它有助于理解类型方差的抽象但重要的概念。


#2 热门回答(10 赞)

采用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的子类型相关并且相反。可以通过更具体的知识引入以下更强的类型。在子类型中。

协方差(在Java中可用)很有用,可以说在子类型中返回更具体的结果;尤其是当A = T且B = S时。逆变法说你准备好处理一个更普遍的论点。


原文链接