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