Kovarianz, Invarianz und Kontravarianz im Klartext erklärt?

113

Heute habe ich einige Artikel über Kovarianz, Kontravarianz (und Invarianz) in Java gelesen. Ich habe den englischen und deutschen Wikipedia-Artikel sowie einige andere Blog-Beiträge und Artikel von IBM gelesen.

Aber ich bin immer noch ein bisschen verwirrt darüber, worum es genau geht? Einige sagen, es geht um die Beziehung zwischen Typen und Untertypen, andere sagen, es geht um die Typkonvertierung und andere sagen, es wird verwendet, um zu entscheiden, ob eine Methode überschrieben oder überladen wird.

Ich suche also nach einer einfachen Erklärung im Klartext, die einem Anfänger zeigt, was Kovarianz und Kontravarianz (und Invarianz) ist. Pluspunkt für ein einfaches Beispiel.

tzrm
quelle
Bitte beziehen Sie sich auf diesen Beitrag, es kann für Sie hilfreich sein: stackoverflow.com/q/2501023/218717
Francisco Alvarado
3
Vielleicht besser die Frage nach dem Stapelaustausch eines Programmierers. Wenn Sie dort posten, sollten Sie angeben, was Sie verstehen und was Sie besonders verwirrt, da Sie gerade jemanden bitten, ein ganzes Tutorial für Sie neu zu schreiben.
Luftkissenfahrzeug voller Aale

Antworten:

288

Einige sagen, es geht um die Beziehung zwischen Typen und Untertypen, andere sagen, es geht um die Typkonvertierung und andere sagen, es wird verwendet, um zu entscheiden, ob eine Methode überschrieben oder überladen wird.

Alles das oben Genannte.

Im Kern beschreiben diese Begriffe, wie die Subtypbeziehung durch Typtransformationen beeinflusst wird. Das heißt, wenn Aund BTypen sind, fist eine Typtransformation und ≤ die Subtyp-Beziehung (dh A ≤ Bbedeutet, dass dies Aein Subtyp von ist B), die wir haben

  • fist kovariant, wenn dies A ≤ Bimpliziertf(A) ≤ f(B)
  • fist kontravariant, wenn dies A ≤ Bimpliziertf(B) ≤ f(A)
  • f ist unveränderlich, wenn keiner der oben genannten Punkte zutrifft

Betrachten wir ein Beispiel. Lassen Sie, f(A) = List<A>wo von Listdeklariert wird

class List<T> { ... } 

Ist fkovariant, kontravariant oder invariant? Kovariante würde bedeuten, dass a List<String>ein Subtyp von ist List<Object>, Kontravariante, dass a List<Object>ein Subtyp von ist, List<String>und Invariante, dass keiner ein Subtyp des anderen ist, dh List<String>und List<Object>nicht konvertierbare Typen sind. In Java ist letzteres wahr, wir sagen (etwas informell), dass Generika unveränderlich sind.

Ein anderes Beispiel. Lass f(A) = A[]. Ist fkovariant, kontravariant oder invariant? Das heißt, ist String [] ein Subtyp von Object [], Object [] ein Subtyp von String [] oder ist keiner der Subtypen des anderen? (Antwort: In Java sind Arrays kovariant)

Das war noch ziemlich abstrakt. Um es konkreter zu machen, schauen wir uns an, welche Operationen in Java in Bezug auf die Subtyp-Beziehung definiert sind. Das einfachste Beispiel ist die Zuordnung. Die Aussage

x = y;

wird nur kompiliert, wenn typeof(y) ≤ typeof(x). Das heißt, wir haben gerade erfahren, dass die Aussagen

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

wird nicht in Java kompiliert, aber

Object[] objects = new String[1];

werden.

Ein weiteres Beispiel, bei dem die Subtypbeziehung von Bedeutung ist, ist ein Methodenaufrufausdruck:

result = method(a);

Informell gesehen wird diese Anweisung ausgewertet, indem dem aersten Parameter der Methode der Wert von zugewiesen wird, dann der Hauptteil der Methode ausgeführt wird und anschließend der Rückgabewert der Methode zugewiesen wird result. Wie die einfache Zuordnung im letzten Beispiel muss die "rechte Seite" ein Untertyp der "linken Seite" sein, dh diese Aussage kann nur gültig sein, wenn typeof(a) ≤ typeof(parameter(method))und returntype(method) ≤ typeof(result). Das heißt, wenn die Methode deklariert ist durch:

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

Keiner der folgenden Ausdrücke wird kompiliert:

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

aber

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

werden.

Ein weiteres Beispiel, bei dem es auf die Subtypisierung ankommt, ist das Überschreiben. Erwägen:

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

wo

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

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

Informell schreibt die Laufzeit dies um in:

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

Damit die markierte Zeile kompiliert werden kann, muss der Methodenparameter der überschreibenden Methode ein Supertyp des Methodenparameters der überschriebenen Methode und der Rückgabetyp ein Subtyp des überschriebenen Methodens sein. Formal f(A) = parametertype(method asdeclaredin(A))muss es zumindest kontravariant sein und f(A) = returntype(method asdeclaredin(A))muss zumindest kovariant sein.

Beachten Sie das "mindestens" oben. Dies sind Mindestanforderungen, die jede vernünftige statisch typsichere objektorientierte Programmiersprache durchsetzen wird, aber eine Programmiersprache kann sich dafür entscheiden, strenger zu sein. Im Fall von Java 1.4 müssen Parametertypen und Methodenrückgabetypen beim Überschreiben von Methoden, dh parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))beim Überschreiben , identisch sein (mit Ausnahme des Löschens von Typen) . Seit Java 1.5 sind beim Überschreiben kovariante Rückgabetypen zulässig, dh Folgendes wird in Java 1.5 kompiliert, jedoch nicht in Java 1.4:

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

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

Ich hoffe, ich habe alles abgedeckt - oder besser gesagt, die Oberfläche zerkratzt. Trotzdem hoffe ich, dass es helfen wird, das abstrakte, aber wichtige Konzept der Typvarianz zu verstehen.

Meriton
quelle
1
Da Java 1.5 kontravariante Argumenttypen beim Überschreiben zulässig sind. Ich denke du hast das verpasst.
Brian Gordon
13
Sind sie? Ich habe es gerade in Eclipse versucht, und der Compiler dachte, ich wollte es eher überladen als überschreiben, und lehnte den Code ab, als ich der Unterklassenmethode eine @ Override-Annotation hinzufügte. Haben Sie Beweise für Ihre Behauptung, dass Java kontravariante Argumenttypen unterstützt?
Meriton
1
Ah, du hast recht. Ich habe jemandem geglaubt, ohne es selbst zu überprüfen.
Brian Gordon
1
Ich habe viel Dokumentation gelesen und ein paar Vorträge zu diesem Thema gesehen, aber dies ist bei weitem die beste Erklärung. Danke vielmals.
Minzchickenflavor
1
+1 für absolut leman und einfach mit A ≤ B. Diese Notation macht die Dinge viel einfacher und aussagekräftiger. Gute Lektüre ...
Romeo Sierra
12

Nehmen Sie das Java-Typ-System und dann Klassen:

Jedes Objekt eines Typs T kann durch ein Objekt des Subtyps T ersetzt werden.

TYP VARIANCE - KLASSENMETHODEN HABEN DIE FOLGENDEN FOLGEN

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;

Man kann sehen, dass:

  • Das T muss vom Subtyp S sein ( kovariant, da B der Subtyp von A ist ).
  • Das V muss der Supertyp von U sein ( Kontravariante als Kontra-Vererbungsrichtung).

Nun beziehen sich Co und Contra auf B als Subtyp von A. Die folgenden stärkeren Typisierungen können mit spezifischerem Wissen eingeführt werden. Im Subtyp.

Kovarianz (in Java verfügbar) ist nützlich, um zu sagen, dass man im Subtyp ein spezifischeres Ergebnis zurückgibt. besonders gesehen, wenn A = T und B = S. Contravariance sagt, dass Sie bereit sind, mit einem allgemeineren Argument umzugehen.

Joop Eggen
quelle
8

Bei Varianz geht es um Beziehungen zwischen Klassen mit unterschiedlichen generischen Parametern. Ihre Beziehungen sind der Grund, warum wir sie besetzen können.

Co- und Contra-Varianz sind ziemlich logische Dinge. Das Sprachtypsystem zwingt uns, die Logik des wirklichen Lebens zu unterstützen. Es ist leicht anhand eines Beispiels zu verstehen.

Kovarianz

Zum Beispiel möchten Sie eine Blume kaufen und haben zwei Blumengeschäfte in Ihrer Stadt: Rosengeschäft und Gänseblümchengeschäft.

Wenn Sie jemanden fragen "Wo ist der Blumenladen?" und jemand sagt dir, wo ist Rosenladen, wäre es okay? Ja, weil Rose eine Blume ist. Wenn Sie eine Blume kaufen möchten, können Sie eine Rose kaufen. Gleiches gilt, wenn Ihnen jemand mit der Adresse des Gänseblümchenladens geantwortet hat. Dies ist beispielsweise der Kovarianz : Sie dürfen auf Guss A<C>zu A<B>, wo Ces eine Unterklasse von B, wenn Agenerische Werte erzeugt (kehrt als Ergebnis aus der Funktion). Bei Covariance geht es um Produzenten.

Typen:

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

Die Frage lautet "Wo ist der Blumenladen?", Die Antwort lautet "Rosenladen dort":

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

Kontravarianz

Zum Beispiel möchten Sie Ihrer Freundin eine Blume schenken. Wenn Ihre Freundin eine Blume liebt, können Sie sie als eine Person betrachten, die Rosen liebt, oder als eine Person, die Gänseblümchen liebt? Ja, denn wenn sie eine Blume liebt, würde sie sowohl Rose als auch Gänseblümchen lieben. Dies ist ein Beispiel für die Kontra : Sie Guss erlaubt sind A<B>zu A<C>, wo Cist Unterklasse von B, wenn Averbraucht generischen Wert. Bei Kontravarianz geht es um Verbraucher.

Typen:

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

}

Sie betrachten Ihre Freundin, die jede Blume liebt, als jemanden, der Rosen liebt, und geben ihr eine Rose:

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

Weitere Informationen finden Sie an der Quelle .

VadzimV
quelle
@ Peter, danke, es ist ein fairer Punkt. Invarianz ist, wenn es keine Beziehungen zwischen Klassen mit unterschiedlichen generischen Parametern gibt, dh Sie können A <B> nicht in A <C> umwandeln, unabhängig von der Beziehung zwischen B und C.
VadzimV