Kowariancja, niezmienność i kontrawariancja wyjaśnione prostym językiem angielskim?

113

Dzisiaj przeczytałem kilka artykułów na temat kowariancji, kontrawariancji (i niezmienności) w Javie. Przeczytałem artykuł w angielskiej i niemieckiej Wikipedii oraz kilka innych postów na blogu i artykułów z IBM.

Ale nadal jestem trochę zdezorientowany, o co dokładnie chodzi? Niektórzy mówią, że chodzi o relacje między typami i podtypami, niektórzy mówią, że chodzi o konwersję typów, a niektórzy twierdzą, że jest to używane do decydowania, czy metoda jest nadpisywana, czy przeciążana.

Szukam więc prostego wyjaśnienia w prostym języku angielskim, które pokaże początkującemu, czym jest kowariancja i kontrawariancja (oraz niezmienność). Plus za łatwy przykład.

tzrm
źródło
Zapoznaj się z tym postem, może być pomocny: stackoverflow.com/q/2501023/218717
Francisco Alvarado
3
Być może lepsze pytanie programisty o typie wymiany stosu. Jeśli piszesz tam, zastanów się nad wyjaśnieniem, co rozumiesz, a co szczególnie cię dezorientuje, ponieważ w tej chwili prosisz kogoś o ponowne napisanie całego samouczka.
Poduszkowiec pełen węgorzy,

Odpowiedzi:

289

Niektórzy twierdzą, że dotyczy relacji między typami i podtypami, inni mówią, że dotyczy konwersji typów, a inni twierdzą, że jest używana do decydowania, czy metoda jest nadpisana, czy przeciążona.

Wszystkie powyższe.

W istocie te terminy opisują, jak na relację podtypów wpływają transformacje typów. Oznacza to, że jeśli Ai Bsą typami, fjest transformacją typu i ≤ relacją podtypu (tj. A ≤ BOznacza, że Ajest podtypem B), mamy

  • fjest kowariantny, jeśli to A ≤ Bsugerujef(A) ≤ f(B)
  • fjest sprzeczne, jeśli to A ≤ Bsugerujef(B) ≤ f(A)
  • f jest niezmienna, jeśli żadna z powyższych nie zachodzi

Rozważmy przykład. Niech f(A) = List<A>gdzie Listjest zadeklarowane przez

class List<T> { ... } 

Czy jest fkowariantny, kontrawariantny czy niezmienny? Kowariantna oznaczałoby, że List<String>jest podtypem List<Object>, kontrawariantny że List<Object>jest podtypem List<String>i niezmienna, że nie jest podtypem druga, czyli List<String>i List<Object>są niewymienialne typy. W Javie to drugie jest prawdą, mówimy (nieco nieformalnie), że typy generyczne są niezmienne.

Inny przykład. Niech f(A) = A[]. Czy jest fkowariantny, kontrawariantny czy niezmienny? To znaczy, czy String [] jest podtypem Object [], Object [] jest podtypem String [], czy też nie jest podtypem drugiego? (Odpowiedź: w Javie tablice są kowariantne)

To wciąż było raczej abstrakcyjne. Aby było to bardziej konkretne, przyjrzyjmy się, które operacje w Javie są zdefiniowane pod względem relacji podtypu. Najprostszym przykładem jest przydział. Twierdzenie

x = y;

skompiluje się tylko wtedy, gdy typeof(y) ≤ typeof(x). Oznacza to, że właśnie dowiedzieliśmy się, że oświadczenia

ArrayList<String> strings = new ArrayList<Object>();
ArrayList<Object> objects = new ArrayList<String>();

nie skompiluje się w Javie, ale

Object[] objects = new String[1];

będzie.

Innym przykładem, w którym relacja podtypu ma znaczenie, jest wyrażenie wywołania metody:

result = method(a);

Mówiąc nieformalnie, ta instrukcja jest oceniana przez przypisanie wartości ado pierwszego parametru metody, następnie wykonanie treści metody, a następnie przypisanie wartości zwracanej metodom result. Podobnie jak zwykłe przypisanie w ostatnim przykładzie, „prawa strona” musi być podtypem „lewej strony”, tj. To stwierdzenie może być ważne tylko wtedy, gdy typeof(a) ≤ typeof(parameter(method))i returntype(method) ≤ typeof(result). To znaczy, jeśli metoda jest zadeklarowana przez:

Number[] method(ArrayList<Number> list) { ... }

żadne z poniższych wyrażeń nie zostanie skompilowane:

Integer[] result = method(new ArrayList<Integer>());
Number[] result = method(new ArrayList<Integer>());
Object[] result = method(new ArrayList<Object>());

ale

Number[] result = method(new ArrayList<Number>());
Object[] result = method(new ArrayList<Number>());

będzie.

Kolejny przykład, w którym znaczenie ma podtytuł jest nadrzędne. Rozważać:

Super sup = new Sub();
Number n = sup.method(1);

gdzie

class Super {
    Number method(Number n) { ... }
}

class Sub extends Super {
    @Override 
    Number method(Number n);
}

Nieformalnie środowisko wykonawcze przepisze to do:

class Super {
    Number method(Number n) {
        if (this instanceof Sub) {
            return ((Sub) this).method(n);  // *
        } else {
            ... 
        }
    }
}

Aby zaznaczony wiersz został skompilowany, parametr metody przesłaniającej metody musi być nadtypem parametru metody zastępowanej metody, a typ zwracany jest podtypem metody przesłoniętej. Formalnie rzecz biorąc, f(A) = parametertype(method asdeclaredin(A))musi być przynajmniej kontrawariantna, a jeśli to f(A) = returntype(method asdeclaredin(A))przynajmniej kowariantna.

Zwróć uwagę na „co najmniej” powyżej. Są to minimalne wymagania, które będzie wymuszał każdy rozsądny, statycznie bezpieczny, obiektowy język programowania, ale język programowania może być bardziej rygorystyczny. W przypadku Javy 1.4 typy parametrów i zwracane metody muszą być identyczne (z wyjątkiem wymazywania typu) podczas przesłonięcia metod, tj. parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))Podczas przesłaniania. Od wersji Java 1.5 kowariantne typy zwracane są dozwolone podczas zastępowania, tj. Następujące elementy będą kompilowane w Javie 1.5, ale nie w Javie 1.4:

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

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

Mam nadzieję, że wszystko pokryłem - a raczej zarysowałem powierzchnię. Wciąż mam nadzieję, że pomoże to zrozumieć abstrakcyjną, ale ważną koncepcję wariancji typu.

meriton
źródło
1
Ponadto, ponieważ kontrawariantne typy argumentów Java 1.5 są dozwolone podczas przesłonięcia. Myślę, że przegapiłeś to.
Brian Gordon
13
Czy oni są? Po prostu wypróbowałem to w eclipse, a kompilator pomyślał, że chcę przeciążać, a nie przesłonić, i odrzuciłem kod, gdy umieściłem adnotację @Override w metodzie podklasy. Czy masz jakieś dowody na to, że Java obsługuje kontrawariantne typy argumentów?
meriton
1
Ach, masz rację. Uwierzyłem komuś, nie sprawdzając tego sam.
Brian Gordon
1
Przeczytałem dużo dokumentacji i obejrzałem kilka referatów na ten temat, ale to jest zdecydowanie najlepsze wyjaśnienie. Wielkie dzięki.
minzchickenflavor
1
+1 za bycie absolutnie leman i prostym z A ≤ B. Ta notacja sprawia, że ​​rzeczy są znacznie prostsze i bardziej znaczące. Dobra lektura ...
Romeo Sierra
12

Biorąc system typów java, a następnie klasy:

Każdy obiekt pewnego typu T można zastąpić obiektem podtypu T.

RÓŻNORODNOŚĆ TYPU - METODY KLAS MAJĄ NASTĘPUJĄCE KONSEKWENCJE

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;

Można zauważyć, że:

  • T musi być podtypem S ( kowariantny, ponieważ B jest podtypem A ).
  • V musi być nadtypem U ( kontrawariantny , jako przeciwny kierunek dziedziczenia).

Teraz współ- i przeciwstawne temu, że B jest podtypem A. Następujące silniejsze typowania można wprowadzić z bardziej szczegółową wiedzą. W podtypie.

Kowariancja (dostępna w Javie) jest przydatna, aby powiedzieć, że zwraca się bardziej szczegółowy wynik w podtypie; szczególnie widoczne, gdy A = T i B = S. Contravariance mówi, że jesteś przygotowany na bardziej ogólny argument.

Joop Eggen
źródło
9

Wariancja dotyczy relacji między klasami o różnych parametrach ogólnych. Ich relacje są powodem, dla którego możemy je obsadzić.

Wariancja Co i Contra to całkiem logiczne rzeczy. System typów języka zmusza nas do wspierania logiki prawdziwego życia. Łatwo to zrozumieć na przykładzie.

Kowariancja

Na przykład chcesz kupić kwiat, a masz w swoim mieście dwa sklepy z kwiatami: sklep z różami i sklep ze stokrotkami.

Jeśli zapytasz kogoś „gdzie jest kwiaciarnia?” a ktoś ci powie, gdzie jest sklep z różami, czy byłoby dobrze? tak, ponieważ róża to kwiat, jeśli chcesz kupić kwiat, możesz kupić różę. To samo dotyczy sytuacji, gdy ktoś przesłał Ci adres sklepu ze stokrotkami. To jest przykład kowariancji : możesz rzutować A<C>na A<B>, gdzie Cjest podklasą B, if Ageneruje wartości ogólne (zwraca jako wynik funkcji). Kowariancja dotyczy producentów.

Rodzaje:

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

Pytanie brzmi „gdzie jest kwiaciarnia?”, Odpowiedź brzmi „tam sklep z różami”:

static FlowerShop<? extends Flower> tellMeShopAddress() {
    return new RoseShop();
}

Sprzeczność

Na przykład chcesz podarować kwiat swojej dziewczynie. Jeśli twoja dziewczyna kocha każdy kwiat, czy możesz uważać ją za osobę, która kocha róże, czy osobę, która kocha stokrotki? tak, ponieważ gdyby kochała jakikolwiek kwiat, pokochałaby zarówno różę, jak i stokrotkę. To jest przykład kontrawariancji : możesz rzucać A<B>do A<C>, gdzie Cjest podklasa B, jeśli Azużywa wartość ogólną. Kontrawariancja dotyczy konsumentów.

Rodzaje:

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!");
    }

}

Uważasz swoją dziewczynę, która kocha każdy kwiat, za kogoś, kto kocha róże i dajesz jej różę:

PrettyGirl<? super Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

Więcej można znaleźć w Źródle .

VadzimV
źródło
@Peter, dzięki, to słuszna uwaga. Niezmienność ma miejsce wtedy, gdy nie ma relacji między klasami o różnych parametrach ogólnych, tj. Nie można rzutować A <B> na A <C> niezależnie od relacji między B i C.
VadzimV