Ковариация, инвариантность и контравариантность объясняются на простом английском языке?

Сегодня я прочитал несколько статей о ковариантности, контравариантности (и инвариантности) в Java. Я прочитал английскую и немецкую статью в Википедии, а также некоторые другие публикации в блоге и статьи от IBM.

Но я все еще немного запутался в том, что это такое? Некоторые говорят об отношениях между типами и подтипами, другие говорят о преобразовании типов, а некоторые говорят, что раньше он решал, переопределен ли метод или перегружен.

Поэтому я ищу простое объяснение на простом английском языке, которое показывает новичку, что такое Ковариация и Контравариантность (и Инвариантность). Плюс за простой пример.

Ответ 1

Некоторые говорят, что речь идет об отношениях между типами и подтипами, другие говорят, что речь идет о преобразовании типов, а другие говорят, что он используется для определения того, является ли метод перезаписанным или перегруженным.

Все вышеперечисленное.

По сути, эти термины описывают, как на отношение подтипа влияют преобразования типов. То есть, если A и B являются типами, 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 ковариантным, контравариантным или инвариантным? Ковариант будет означать, что 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;

скомпилируется только если 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() { ... }
}

Я надеюсь, что я покрыл все - или, скорее, поцарапал поверхность. Тем не менее, я надеюсь, что это поможет понять абстрактную, но важную концепцию дисперсии типов.

Ответ 2

Взятие системы типа 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. Контравариантность говорит, что вы готовы справиться с более общим аргументом.

Ответ 3

Дисперсия касается отношений между классами с различными обобщенными параметрами. Их отношения - причина, по которой мы можем их использовать.

Дисперсия Co и Contra - довольно логичные вещи. Система языковых типов заставляет нас поддерживать логику реальной жизни. Это легко понять на примере.

ковариации

Например, вы хотите купить цветок, и у вас есть два магазина цветов в вашем городе: магазин роз и магазин ромашек.

Если вы спросите кого-то "где магазин цветов?" а кто-то скажет вам, где магазин роз, все будет в порядке? да, потому что роза - это цветок, если вы хотите купить цветок, вы можете купить розу. То же самое относится, если кто-то ответил вам адресом магазина ромашки. Это пример ковариации: вам разрешено приводить A<C> к A<B>, где C является подклассом B, если 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<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());

Вы можете найти больше в Источнике.