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 A
und B
Typen sind, f
ist eine Typtransformation und ≤ die Subtyp-Beziehung (dh A ≤ B
bedeutet, dass dies A
ein Subtyp von ist B
), die wir haben
f
ist kovariant, wenn dies A ≤ B
impliziertf(A) ≤ f(B)
f
ist kontravariant, wenn dies A ≤ B
impliziertf(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 List
deklariert wird
class List<T> { ... }
Ist f
kovariant, 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 f
kovariant, 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 a
ersten 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.