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