Коваріація, інваріантність та протиріччя пояснюються простою англійською мовою?


113

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

Але я все ще трохи розгублений, про що саме йдеться? Деякі кажуть, що мова йде про взаємозв'язок типів і підтипів, деякі кажуть, що це перетворення типів, а деякі кажуть, що він використовується для вирішення питання про те, чи метод перекритий чи перевантажений.

Тому я шукаю просте пояснення простою англійською мовою, яке показує новачкам, що таке коваріація та противагу (та інваріантність). Плюс бала за простий приклад.


Будь ласка, зверніться до цієї публікації, вона може бути корисною для вас: stackoverflow.com/q/2501023/218717
Франциско Альварадо

3
Можливо, краще питання типу обміну стеками програміста. Якщо ви там дописуєтесь, подумайте про те, що саме ви розумієте, і що конкретно вас бентежить, адже саме зараз ви просите когось переписати цілий підручник для вас.
Hovercraft Full Of Eels

Відповіді:


288

Одні кажуть, що мова йде про взаємозв'язок типів і підтипів, інші кажуть, що це перетворення типів, а інші кажуть, що він використовується для визначення того, чи метод буде перезаписаний чи перевантажений.

Все вищеперераховане.

В основі цих термінів описано, як на підтипове вплив впливають перетворення типів. Тобто, якщо 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>є незворотними типами. На Яві останнє вірно, ми говоримо (дещо неофіційно), що дженерики є інваріантними.

Ще один приклад. Нехай 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() { ... }
}

Я сподіваюся, що я все накрив, а точніше - почухав поверхню. І все-таки сподіваюся, що це допоможе зрозуміти абстрактне, але важливе поняття дисперсії типу.


1
Крім того, оскільки Java 1.5 контраваріантні типи аргументів дозволені при переопределенні. Я думаю, ти пропустив це.
Брайан Гордон

13
Чи вони? Я просто спробував це у затемненні, і компілятор вважав, що маю намір перевантажити, а не переосмислити, і відхилив код, коли помістив анотацію @Override на метод підкласу. Чи є у вас якісь докази вашої заяви про те, що Java підтримує противаріантні типи аргументів?
Мерітон

1
Ах, ти маєш рацію. Я вірив комусь, не перевіряючи його сам.
Брайан Гордон

1
Я прочитав багато документації та переглянув кілька бесід на цю тему, але це, безумовно, найкраще пояснення. Thnx багато.
minzchickenflavor

1
+1 за те, що ви абсолютно леман і просто A ≤ B. Це позначення робить речі набагато простішими та змістовнішими. Добре читайте ...
Ромео Сьєрра

12

Беручи систему типу java, а потім класи:

Будь-який об'єкт певного типу Т може бути заміщений об'єктом підтипу Т.

ВИМОГА ВИДУ

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


8

Варіантність стосується зв’язків між класами з різними параметрами генерики. Їхні стосунки - це причина, чому ми можемо їх кинути.

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

Ви можете знайти більше у Джерело .


@Peter, спасибі, це справедливо. Інваріантність - це коли немає зв’язків між класами з різними загальними параметрами, тобто ви не можете привласнити A <B> до A <C> незалежно від співвідношення між B і C.
ВадзімВ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.