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