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

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

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

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

2 ответа

Решение

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

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

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

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

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

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

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

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

Другие вопросы по тегам