Ko- und Kontravarianz in Java

Hallo Leute,

ich habe etwas Schwierigkeiten mit dem Verständnis des Ko- und Kontravarianz Konzeptes in Java. Unser Uniskript formuliert das ganze etwas seltsam. Aber bevor ich euch nach korrekter Definition der Begriffe frage, frage ich euch lieber, ob ich das Konzept anhand der folgenden Beispiele verstanden habe.

Beispiel 1) Kovarainz für Ergebnistypen


Class A { ... }
Class B extends A { ... }
Class Mensch 
{
     public A methode()
    {
        ...
        return A;
    }
}
Class Kind extends Mensch 
{ 
    // Diese Redeklaration von methode ist konform, weil die Ergebnistypen kovariant angepasst wurden.
    public B methode()
    {
        ...
        return B;
    }
}

Beispiel 1 wäre falsch, wenn ich die Ergebnistypen kontravariant anpasse.


Class A { ... }
Class B extends A { ... }
Class Mensch 
{
     public B methode()
    {
        ...
        return B;
    }
}
Class Kind extends Mensch 
{ 
    // Diese Redeklaration von methode ist konform, weil die Ergebnistypen kovariant angepasst wurden.
    public A methode()
    {
        ...
        return A;
    }
}

Beipsiel 2) Kontravarianz für Typen von Parametern


Class A { ... }
Class B extends A { ... }

Class Obst 
{
     public void pfluecken(B obst)
     { 
          ...
     }
}

Class Apfel extends Obst
{
     // Diese Redeklaration ist konform, weil die Parametertypen Kontravariant angepasst wurden.
     public void pfluecken(A obst)
     {
          ...
     }
}

Beispiel 2 wäre falsch, wenn ich die Parametertypen kovariant anpasse:


Class A { ... }
Class B extends A { ... }

Class Obst 
{
     public void pfluecken(A obst)
     { 
          ...
     }
}

Class Apfel extends Obst
{
     // Diese Redeklaration ist konform, weil die Parametertypen Kontravariant angepasst wurden.
     public void pfluecken(B obst)
     {
          ...
     }
}

Also Zusammengefasst: Ergebnistypen dürfen kovariant angepasst werden, aber nicht kontravariant. Parametertypen dürfen kontravariant angepasst werden, aber nicht kovariant.

Java-Code-Tags wäre bei sowas nützlich, aber auch nur wenn halbwegs korrekte Klasse geschrieben, etwa mit kleinem class,
schon selber programmiert, ein System zum Ausprobieren zur Verfügung?

oder keinen Wert auf kleines class hier gelegt? :wink: besser ja doch

unmodifizierte Kommentare a la
// Diese Redeklaration von methode ist konform, weil die Ergebnistypen kovariant angepasst wurden.
bei just Veränderung des zugehörigen Codes in verschiedenen Beispielen ist äußerst ungünstig…


für Beispiel 1 liegst du richtig,

bei den Parametern ist es schwieriger, weil Methoden sich nicht zwingend überschreiben müssen,
beide Varianten von Beispiel 2 enthalten keine Methodenüberschreibung,
beide kompilieren, wie du selber hättest testen können wenn vollständig gebaut,

dass die beiden Klasen in Verbindung stehen spielt bei der Methodendefinition kaum eine Rolle,
für Java nicht viel anderes als zusätzlich noch pfluecken(String) oder pfluecken(int) oder auch pfluecken(A, B) definiert

nur bei der Auswahl der Methode bei einem bestimmten Aufruf (statische Bindung) wird es etwas spannend,
da wird ja die passenste Methode ausgewählt,
für B ist eine B-Methode besser passend als eine A-Methode (und viel besser als eine String-Methode…)

bei Parametertypen findet keine Anpassung vergleichbar mit Ergebnistypen statt,
überschrieben werden kann nur mit exakt gleichen Parameter/ Signatur (während Rückgabewert geändert werden darf),
jede Parameter-Abweichung ist eine andere Methode, höhere wie auch niedrigere Klassen oder auch ganz andere Parameter erlaubt

edit:
in Kovarianz und Kontravarianz – Wikipedia ist auch davon die Rede

Typsicherheit bei Methoden

Auf Grund der Eigenschaften des Substitutionsprinzipes ist statische Typsicherheit dann gewährleistet, wenn die Argumenttypen kontravariant und die Ergebnistypen kovariant sind.

aber bei Java passt das nicht recht, jedenfalls nicht hinsichtlich Kompilieren/ Überschreiben von Methoden

Eigentlich ist das Konzept ganz einfach zu verstehen:

Nehmen wir das Interface Function, das ungefähr so aussieht:

   public Output apply(Input input);
} 

Nun stellen wir uns vor, an einer Stelle würde eine Function<Number, Number> benötigt, man hat also an dieser Stelle eine Number als Eingabewert, und will dann von der Funktion eine andere Number zurückgegeben haben, die man dann weiterverarbeitet.

Wenn wir jetzt an dieser Stelle keine Function<Number, Number>, sondern eine Function<Object, Number> übergeben bekommen, wäre das nicht schlimm: Wir können immer noch unsere Zahl als Argument von apply übergeben, auch wenn der Input-Typ “allgemeiner” ist, als wir eigentlich brauchen. Deshalb sind Eingabewerte immer contravariant, auch in anderen Zusammenhängen (z.B. bei Consumer oder Comparator).

Wenn wir nun statt Function<Number, Number> eine Function<Number, Integer> vorgesetzt bekommen, ist das auch nicht weiter schlimm: Der Rückgabewert von apply ist zwar “spezieller” als wir brauchen, aber das stört bei der Weiterverarbeitung nicht. Deshalb sind Ausgabewerte immer covariant (z.B. auch bei Supplier).

In unserem Beispiel wäre die Typangabe Function<Number, Number> für die gewünschte Funktion also zu restriktiv, weil sie andere, ebenfalls verwendbare Funktionen ausschließen würde. Deshalb kann man die gewünschten Varianzen mittels Wildcards ausdrücken, in unserem Fall vorn contravariant und hinten covariant: Function<? super Number, ? extends Number>.

Auch wenn es noch ein paar Feinheiten gibt, ist eigentlich nicht viel mehr dran. Mit ein bisschen Überlegung kommt man schnell drauf, wo welche Varianz gebraucht wird. Nehmen wir z.B. eine Methode, die alle Elemente einer Liste in eine andere packt:

public <A> void copy(List<A> source, List<A> target) {
  for (A a: source) {
    target.add(a);  //ja, es ginge auch einfacher mit addAll, ist ja nur ein Beispiel
  }
}

Könnte eine der Listen “speziellere” oder “allgemeinere” Elemente beinhalten? Ja! In der Liste source wird der Typ A nur gelesen, es ist also ein Ausgabetyp und damit covariant. In der Liste target wird der Typ A nur geschrieben, es ist also ein Eingabetyp und damit contravariant. Die Version mit Varianzen sieht also so aus:

public <A> void copy(List<? extends A> source, List<? super A> target) {
  for (A a: source) {
    target.add(a);  
  }
}

Wenn dir das für einen abstrakten Typ schwerfällt, setze gedanklich einen konkreten Typ wie Number ein (dann siehst du, dass man eine Integer-Liste in eine Objekt-Liste kopieren kann, aber nicht umgekehrt).

@SlaterB Ich nehme an, dass Java Code Tags die Java Schlüsselwörter hervorheben würden oder? Das habe ich nicht gewusst.

Stimmt, class muss ja klein geschrieben werden. Offensichtlich ist der Code nicht getestet, sonst hätte der Compiler ja gemeckert :slight_smile:

dass die beiden Klasen in Verbindung stehen spielt bei der Methodendefinition kaum eine Rolle,
für Java nicht viel anderes als zusätzlich noch pfluecken(String) oder pfluecken(int) oder auch pfluecken(A, B) definiert

Das heisst, dass es sich hier nicht um eine Methodenredeklaration handelt? Das Schlüsselwort extends sagt doch im Grunde, dass ich alle Methoden und Felder der Superklasse in meine Subklasse übertrage. Wenn ich jetzt in meiner Subklasse eine weitere Methode deklariere, die den gleichen Namen hat wie die geerbte, führe ich doch eine Redeklaration durch. Das nennt man doch Methode überladen. Oder liege ich da falsch?

@Landei Danke für die Erklärung, aber ich habe Schwierigkeiten mit dem Verständnis der Syntax, die du verwendest. Diese Geschichten mit „<“ und „>“ habe ich mir noch nicht angesehen. Ich werde mal etwas mit dem Compiler herumexperementieren und sehen was geht und was nicht.

habe ich wirklich ‚Klasen‘ geschrieben? uiui

Überladen von Methoden ist in der Tat ein Konzept, aber eins was es genauso innerhalb einer Klasse gibt,
dafür braucht es keine Unterklassen

Unterklassen dürften freilich auch Methoden der Oberklasse überladen und beliebig weitere Methoden definieren,
der springende Punkt der Polymorphie ist aber das Überschreiben von Methoden, so dass Code anstelle dessen in der Oberklasse ausgeführt wird,

edit: ok, viel steht da nicht, hier mehr zu Java:
Überladen von Methoden
aber da steht auch nicht viel, weil gar nicht so viel zu sagen ist :wink: , na man könnte noch mehr suchen

‚Redeklaration‘ kann ich dazu nicht eindeutig einordnen, könnte für beides stehen,
Suchmaschinenergebnisse äußerst dünn dazu…


Überladen finde ich nicht ganz so spannend, z.B. sind da auch völlig unterschiedliche Rückgabetypen erlaubt


   public String pfluecken(B obst)

und es spielt kaum eine Rolle ob in einer Klasse oder in Unterklasse,

es ist praktisch alles erlaubt, außer 2x dieselbe Methode zu definieren

Überladen ist auch unterschiedliche Parameteranzahl,
dass gleiche Anzahl und die Parameter in Klassenbeziehung stehen wird meist gar nicht extra betrachtet,

für statische vs. dynamische Bindung aber interessant, dazu könnte man auch nachlesen,
aber ob das hier so genau passt?..


beim Überschreiben ist dagegen eben aufzupassen, der Rückgabewert darf abweichen, aber nur ‚kovariant‘ (genau dein Beispiel 1),
bei den Parametern gibt es aber hierzu keinen Spielraum, nur exakt gleiche Signatur erlaubt,
sonst wird eine Methode nicht überschrieben/ überdeckt


das ist aber nur die programmiertechnische Seite der Methodendeklarationen,

die von Landei auch interessant, mal auf die Schnelle noch kurz anders formuliert:
ich bin ich und hantiere mit Number,

  • ich will Number übergeben, an wen? nur an die, die Number oder deren Oberklassen akzeptieren,
  • ich will Number zurück(haben), von wem? nur von denen, die Number oder deren Unterklassen (zurück)geben

[QUOTE=skonline90]
@Landei Danke für die Erklärung, aber ich habe Schwierigkeiten mit dem Verständnis der Syntax, die du verwendest. Diese Geschichten mit “<” und “>” habe ich mir noch nicht angesehen. Ich werde mal etwas mit dem Compiler herumexperementieren und sehen was geht und was nicht.[/QUOTE]

Generics. Da wird einfach eine Variable für einen konkreten Typ eingesetzt. So braucht man z.B. keine einzelnen Klassen für “Liste von Strings”, “Liste von Integers”, “Liste von Dates” schreiben, sondern für eine allgemeine “Liste von A” (mit der Syntax List<A>). Will man diese Klasse dann verwenden, muss man natürlich sagen, welchen konkreten Typ man dort haben will, also z.B. List<String>. Das gleiche kann man auch auf der Ebene einzelner Methoden haben. Für die generischen Typvariablen kann man dann noch bestimmte “Bedingungen” angeben, so kann MyClass<A extends Number> nur mit konkreten Typen instanziiert werden, die Unterklassen von Number sind.