Alguns dizem que é sobre relacionamento entre tipos e subtipos, outros dizem que é sobre conversão de tipo e outros dizem que é usado para decidir se um método é sobrescrito ou sobrecarregado.
Tudo acima.
No fundo, esses termos descrevem como a relação de subtipo é afetada pelas transformações de tipo. Ou seja, se A
e B
são tipos, f
é uma transformação de tipo, e ≤ a relação de subtipo (ou seja, A ≤ B
significa que A
é um subtipo de B
), temos
f
é covariante se A ≤ B
implica quef(A) ≤ f(B)
f
é contravariante se A ≤ B
implica quef(B) ≤ f(A)
f
é invariante se nenhuma das opções acima for válida
Vamos considerar um exemplo. Deixe f(A) = List<A>
onde List
é declarado por
class List<T> { ... }
É f
covariante, contravariante ou invariante? Covariante significaria que a List<String>
é um subtipo de List<Object>
, contravariante que a List<Object>
é um subtipo de List<String>
e invariante que nenhum é um subtipo do outro, ou seja, List<String>
e List<Object>
são tipos inconversíveis. Em Java, o último é verdade, dizemos (um tanto informalmente) que os genéricos são invariantes.
Outro exemplo. Deixe f(A) = A[]
. É f
covariante, contravariante ou invariante? Ou seja, String [] é um subtipo de Object [], Object [] um subtipo de String [], ou nenhum é um subtipo do outro? (Resposta: Em Java, os arrays são covariantes)
Isso ainda era bastante abstrato. Para tornar isso mais concreto, vamos examinar quais operações em Java são definidas em termos da relação de subtipo. O exemplo mais simples é a atribuição. A declaração
x = y;
irá compilar apenas se typeof(y) ≤ typeof(x)
. Ou seja, acabamos de saber que as declarações
ArrayList<String> strings = new ArrayList<Object>();
ArrayList<Object> objects = new ArrayList<String>();
não vai compilar em Java, mas
Object[] objects = new String[1];
vai.
Outro exemplo onde a relação de subtipo é importante é uma expressão de invocação de método:
result = method(a);
Falando informalmente, essa instrução é avaliada atribuindo o valor de a
ao primeiro parâmetro do método, executando o corpo do método e atribuindo o valor de retorno do método a result
. Como a atribuição simples no último exemplo, o "lado direito" deve ser um subtipo do "lado esquerdo", ou seja, esta instrução só pode ser válida se typeof(a) ≤ typeof(parameter(method))
e returntype(method) ≤ typeof(result)
. Ou seja, se o método for declarado por:
Number[] method(ArrayList<Number> list) { ... }
nenhuma das seguintes expressões irá compilar:
Integer[] result = method(new ArrayList<Integer>());
Number[] result = method(new ArrayList<Integer>());
Object[] result = method(new ArrayList<Object>());
mas
Number[] result = method(new ArrayList<Number>());
Object[] result = method(new ArrayList<Number>());
vai.
Outro exemplo em que a subtipagem é importante é a substituição. Considerar:
Super sup = new Sub();
Number n = sup.method(1);
Onde
class Super {
Number method(Number n) { ... }
}
class Sub extends Super {
@Override
Number method(Number n);
}
Informalmente, o tempo de execução irá reescrever isso para:
class Super {
Number method(Number n) {
if (this instanceof Sub) {
return ((Sub) this).method(n); // *
} else {
...
}
}
}
Para a linha marcada para compilar, o parâmetro do método do método sobrescrito deve ser um supertipo do parâmetro do método do método sobrescrito, e o tipo de retorno um subtipo do método sobrescrito. Falando formalmente, f(A) = parametertype(method asdeclaredin(A))
deve ser pelo menos contravariante, e se f(A) = returntype(method asdeclaredin(A))
deve ser pelo menos covariante.
Observe o "pelo menos" acima. Esses são requisitos mínimos que qualquer linguagem de programação orientada a objetos segura de tipo estaticamente razoável irá impor, mas uma linguagem de programação pode optar por ser mais estrita. No caso do Java 1.4, os tipos de parâmetro e tipos de retorno de método devem ser idênticos (exceto para eliminação de tipo) ao substituir métodos, ou seja, parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))
ao substituir. Desde Java 1.5, os tipos de retorno covariant são permitidos durante a substituição, ou seja, o seguinte será compilado em Java 1.5, mas não em Java 1.4:
class Collection {
Iterator iterator() { ... }
}
class List extends Collection {
@Override
ListIterator iterator() { ... }
}
Espero ter coberto tudo - ou melhor, arranhado a superfície. Ainda assim, espero que ajude a entender o conceito abstrato, mas importante, de variância de tipo.