Ответ 1
Некоторые говорят, что речь идет об отношениях между типами и подтипами, другие говорят, что речь идет о преобразовании типов, а другие говорят, что он используется для определения того, является ли метод перезаписанным или перегруженным.
Все вышеперечисленное.
По сути, эти термины описывают, как на отношение подтипа влияют преобразования типов. То есть, если 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>
- это необратимые типы. В Java последнее верно, мы говорим (неформально), что генерики являются инвариантными.
Другой пример. Пусть 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() { ... }
}
Я надеюсь, что я покрыл все - или, скорее, поцарапал поверхность. Тем не менее, я надеюсь, что это поможет понять абстрактную, но важную концепцию дисперсии типов.