Algunos dicen que se trata de la relación entre tipos y subtipos, otros dicen que se trata de conversión de tipos y otros dicen que se usa para decidir si un método se sobrescribe o se sobrecarga.
Todas las anteriores.
En el fondo, estos términos describen cómo la relación de subtipo se ve afectada por las transformaciones de tipo. Es decir, si A
y B
son tipos, f
es una transformación de tipo, y ≤ la relación de subtipo (es decir, A ≤ B
significa que A
es un subtipo de B
), tenemos
f
es covariante si A ≤ B
implica quef(A) ≤ f(B)
f
es contravariante si A ≤ B
implica quef(B) ≤ f(A)
f
es invariante si ninguno de los anteriores se cumple
Consideremos un ejemplo. Deja f(A) = List<A>
donde List
es declarado por
class List<T> { ... }
¿Es f
covariante, contravariante o invariante? Covariante significaría que a List<String>
es un subtipo de List<Object>
, contravariante que a List<Object>
es un subtipo List<String>
e invariante que ninguno es un subtipo del otro, es decir, List<String>
y List<Object>
son tipos inconvertibles. En Java, esto último es cierto, decimos (un tanto informalmente) que los genéricos son invariantes.
Otro ejemplo. Deja f(A) = A[]
. ¿Es f
covariante, contravariante o invariante? Es decir, ¿String [] es un subtipo de Object [], Object [] es un subtipo de String [], o ninguno es un subtipo del otro? (Respuesta: en Java, las matrices son covariantes)
Esto todavía era bastante abstracto. Para hacerlo más concreto, veamos qué operaciones en Java se definen en términos de la relación de subtipo. El ejemplo más simple es la asignación. La declaración
x = y;
se compilará solo si typeof(y) ≤ typeof(x)
. Es decir, acabamos de enterarnos de que las declaraciones
ArrayList<String> strings = new ArrayList<Object>();
ArrayList<Object> objects = new ArrayList<String>();
no se compilará en Java, pero
Object[] objects = new String[1];
será.
Otro ejemplo donde la relación de subtipo es importante es una expresión de invocación de método:
result = method(a);
Hablando informalmente, esta declaración se evalúa asignando el valor de a
al primer parámetro del método, luego ejecutando el cuerpo del método y luego asignando el valor de retorno de los métodos a result
. Al igual que la asignación simple en el último ejemplo, el "lado derecho" debe ser un subtipo del "lado izquierdo", es decir, esta declaración solo puede ser válida si typeof(a) ≤ typeof(parameter(method))
y returntype(method) ≤ typeof(result)
. Es decir, si el método es declarado por:
Number[] method(ArrayList<Number> list) { ... }
ninguna de las siguientes expresiones se compilará:
Integer[] result = method(new ArrayList<Integer>());
Number[] result = method(new ArrayList<Integer>());
Object[] result = method(new ArrayList<Object>());
pero
Number[] result = method(new ArrayList<Number>());
Object[] result = method(new ArrayList<Number>());
será.
Otro ejemplo en el que la subtipificación importa es primordial. Considerar:
Super sup = new Sub();
Number n = sup.method(1);
dónde
class Super {
Number method(Number n) { ... }
}
class Sub extends Super {
@Override
Number method(Number n);
}
De manera informal, el tiempo de ejecución reescribirá esto en:
class Super {
Number method(Number n) {
if (this instanceof Sub) {
return ((Sub) this).method(n); // *
} else {
...
}
}
}
Para que la línea marcada se compile, el parámetro de método del método reemplazado debe ser un supertipo del parámetro de método del método reemplazado, y el tipo de retorno un subtipo del método reemplazado. Hablando formalmente, f(A) = parametertype(method asdeclaredin(A))
al menos debe ser contravariante, y si f(A) = returntype(method asdeclaredin(A))
debe ser al menos covariante.
Tenga en cuenta el "al menos" de arriba. Esos son requisitos mínimos que cualquier lenguaje de programación orientado a objetos seguro de tipo estático razonable hará cumplir, pero un lenguaje de programación puede optar por ser más estricto. En el caso de Java 1.4, los tipos de parámetros y los tipos de retorno de métodos deben ser idénticos (excepto para el borrado de tipos) cuando se anulan los métodos, es decir, parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))
cuando se anulan. Desde Java 1.5, los tipos de retorno covariantes están permitidos al anular, es decir, lo siguiente se compilará en Java 1.5, pero no en Java 1.4:
class Collection {
Iterator iterator() { ... }
}
class List extends Collection {
@Override
ListIterator iterator() { ... }
}
Espero haber cubierto todo, o mejor dicho, haber rayado la superficie. Aún así, espero que ayude a comprender el concepto abstracto, pero importante, de variación de tipos.