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

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

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

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


person Community    schedule 12.12.2011    source источник
comment
См. Этот пост, он может быть вам полезен: stackoverflow.com/q/2501023/218717   -  person Francisco Alvarado    schedule 13.12.2011
comment
Возможно, лучше вопрос типа обмена стеками программиста. Если вы публикуете там пост, подумайте о том, чтобы указать, что именно вы понимаете и что конкретно вас смущает, потому что прямо сейчас вы просите кого-нибудь переписать для вас целое руководство.   -  person Hovercraft Full Of Eels    schedule 13.12.2011


Ответы (3)


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

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

По сути, эти термины описывают, как на отношение подтипов влияют преобразования типов. То есть, если 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() { ... }
}

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

person meriton    schedule 12.12.2011
comment
Кроме того, начиная с Java 1.5, при переопределении разрешены контравариантные типы аргументов. Я думаю, ты это пропустил. - person Brian Gordon; 07.05.2013
comment
Они? Я просто попробовал это в eclipse, и компилятор решил, что я имел в виду перегрузку, а не переопределение, и отклонил код, когда я поместил аннотацию @Override в метод подкласса. Есть ли у вас доказательства того, что Java поддерживает контравариантные типы аргументов? - person meriton; 08.05.2013
comment
Ах ты прав. Я кому-то поверил, не проверив сам. - person Brian Gordon; 08.05.2013
comment
Отличные ответы +1 BTW в java обрабатываются как выражения, а не как утверждения, поэтому x = y и ArrayList ‹String› strings = new ArrayList ‹Object› () на самом деле являются выражениями - person Igor; 01.10.2016
comment
@meriton, так что - для простого старичка - контравариантность (по вашему объяснению) - это просто возможность назначить менее конкретный тип (родительский) более конкретному типу, верно? Если это так, то есть я понял, что вы написали, вам следует прочитать ужасную работу MSDN, чтобы попытаться объяснить то же самое на C #. Обобщения (а не коллекции) в C # слишком упрямые. - person Thomas; 14.06.2017
comment
Я прочитал много документации и посмотрел несколько разговоров на эту тему, но это, безусловно, лучшее объяснение. Спасибо большое. - person minzchickenflavor; 14.03.2019
comment
+1 за то, что он абсолютно луман и прост с A ≤ B. Эта запись делает вещи намного более простыми и значимыми. Хорошо читать... - person Romeo Sierra; 05.04.2019
comment
Я знаю, что это очень старый ответ, но, пожалуйста, может кто-нибудь помочь с линией. the method parameter of the overriding method must be a supertype of the method parameter of the overridden method. Из моего понимания Java, параметры метода как Overridden, так и Overriding должны быть одного типа. - person Mab; 11.06.2021
comment
@Mab: я объясняю это в следующем абзаце (тот, который отмечен как минимум выше) - person meriton; 11.06.2021
comment
Спасибо за ответ, сначала я был сбит с толку, так как он следует за заявлением: For the marked line to compile. Я думаю, что теперь из объяснения я понимаю, что ограничения параметра метода Overridden и Overriding, не являющегося Contravariant, характерны для Java и другого языка, который выбирает - person Mab; 11.06.2021

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

Дисперсия 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());

Дополнительную информацию можно найти в Источник.

person VadzimV    schedule 04.10.2019
comment
@ Питер, спасибо, это справедливый вопрос. Инвариантность - это когда нет отношений между классами с разными общими параметрами, то есть вы не можете преобразовать A ‹B› в A ‹C› независимо от отношения между B и C. - person VadzimV; 03.05.2020

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

person Joop Eggen    schedule 12.12.2011