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

2022-08-31 07:40:38

今天,我读了一些关于Java中的协方差,逆变(和不变性)的文章。我阅读了英语和德语维基百科的文章,以及IBM的其他一些博客文章和文章。

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

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


答案 1

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

以上所有。

从本质上讲,这些术语描述了子类型关系如何受到类型转换的影响。也就是说,如果 和 是类型,则是类型转换,并且≤子类型关系(即 表示是 的子类型),我们有ABfA ≤ BAB

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

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

class List<T> { ... } 

是协变、逆变还是不变?协变意味着 a 是 的子类型,逆变 a 是 的子类型,而不变量都不是另一个的子类型,即 并且是不可转换的类型。在Java中,后者是正确的,我们说(有点非正式地)泛型是不变的。fList<String>List<Object>List<Object>List<String>List<String>List<Object>

另一个例子。让。是协变、逆变还是不变?也就是说,String[] 是 Object[] 的子类型,Object[] 是 String[] 的子类型,还是两者都不是另一个的子类型?(答:在Java中,数组是协变的)f(A) = A[]f

这仍然相当抽象。为了更具体,让我们看看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);

非正式地说,此语句的计算方法是将 的值分配给方法的第一个参数,然后执行方法的主体,然后将方法返回值赋值给 。与上一个示例中的普通赋值一样,“右侧”必须是“左侧”的子类型,即此语句只有在 和 的情况下才有效。也就是说,如果方法由以下方式声明:aresulttypeof(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 中不会编译:parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))

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

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

我希望我涵盖了一切 - 或者更确切地说,划伤了表面。我仍然希望它能帮助理解抽象但重要的类型方差概念。


答案 2

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

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

协方差

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

如果你问别人“花店在哪里?”,有人告诉你玫瑰店在哪里,可以吗?是的,因为玫瑰是一朵花,如果你想买一朵花,你可以买一朵玫瑰。如果有人回复您雏菊店的地址,情况也是如此。这是协方差的示例:如果生成泛型值(作为函数的结果返回),则可以将其转换为 ,其中 是 的子类。协方差是关于生产者的。A<C>A<B>CBA

类型:

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>CBA

类型:

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

您可以在上找到更多信息。


推荐