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