¿Covarianza, invarianza y contravarianza explicadas en un lenguaje sencillo?

113

Hoy, leí algunos artículos sobre Covarianza, Contravarianza (e Invarianza) en Java. Leí el artículo de Wikipedia en inglés y alemán, y algunas otras publicaciones de blog y artículos de IBM.

¿Pero todavía estoy un poco confundido sobre de qué se tratan exactamente? Algunos dicen que se trata de la relación entre tipos y subtipos, algunos dicen que se trata de conversión de tipos y algunos dicen que se usa para decidir si un método se anula o se sobrecarga.

Así que estoy buscando una explicación fácil en un lenguaje sencillo, que muestre a un principiante qué es la covarianza y la contravarianza (e invariancia). Punto más para un ejemplo sencillo.

tzrm
fuente
Consulte esta publicación, puede ser útil para usted: stackoverflow.com/q/2501023/218717
Francisco Alvarado
3
Quizás sea mejor la pregunta del tipo de intercambio de pila de un programador. Si publicas allí, considera decir lo que entiendes y lo que te confunde específicamente, porque ahora mismo le estás pidiendo a alguien que vuelva a escribir un tutorial completo para ti.
Aerodeslizador lleno de anguilas

Respuestas:

288

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 Ay Bson tipos, fes una transformación de tipo, y ≤ la relación de subtipo (es decir, A ≤ Bsignifica que Aes un subtipo de B), tenemos

  • fes covariante si A ≤ Bimplica quef(A) ≤ f(B)
  • fes contravariante si A ≤ Bimplica quef(B) ≤ f(A)
  • f es invariante si ninguno de los anteriores se cumple

Consideremos un ejemplo. Deja f(A) = List<A>donde Listes declarado por

class List<T> { ... } 

¿Es fcovariante, 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 fcovariante, 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 aal 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.

meriton
fuente
1
Además, desde Java 1.5, se permiten tipos de argumentos contravariantes al anular. Creo que te lo perdiste.
Brian Gordon
13
¿Son ellos? Lo intenté en eclipse, y el compilador pensó que tenía la intención de sobrecargar en lugar de anular, y rechazó el código cuando coloqué una anotación @Override en el método de subclase. ¿Tiene alguna evidencia para su afirmación de que Java admite tipos de argumentos contravariantes?
meriton
1
Ah, tienes razón. Creí a alguien sin comprobarlo yo mismo.
Brian Gordon
1
Leí mucha documentación y vi algunas charlas sobre este tema, pero esta es, con mucho, la mejor explicación. Muchas gracias.
minzchickenflavor
1
+1 por ser absolutamente leman y sencillo con A ≤ B. Esa notación hace que las cosas sean mucho más simples y significativas. Buena lectura ...
Romeo Sierra
12

Tomando el sistema de tipos java, y luego clases:

Cualquier objeto de algún tipo T puede ser sustituido por un objeto del subtipo T.

VARIENCIA DE TIPO - LOS MÉTODOS DE CLASE TIENEN LAS SIGUIENTES CONSECUENCIAS

class A {
    public S f(U u) { ... }
}

class B extends A {
    @Override
    public T f(V v) { ... }
}

B b = new B();
t = b.f(v);
A a = ...; // Might have type B
s = a.f(u); // and then do V v = u;

Se puede ver que:

  • La T debe ser subtipo S ( covariante, ya que B es subtipo de A ).
  • La V debe ser supertipo de U ( contravariante , como dirección de herencia contraria).

Ahora co- y contra- se relacionan con que B sea subtipo de A. Los siguientes tipos más fuertes pueden introducirse con un conocimiento más específico. En el subtipo.

La covarianza (disponible en Java) es útil, para decir que uno devuelve un resultado más específico en el subtipo; especialmente visto cuando A = T y B = S. La contravarianza dice que está preparado para manejar un argumento más general.

Joop Eggen
fuente
8

La varianza se trata de relaciones entre clases con diferentes parámetros genéricos. Sus relaciones son la razón por la que podemos elegirlos.

La varianza Co y Contra son cosas bastante lógicas. El sistema de tipos de lenguaje nos obliga a apoyar la lógica de la vida real. Es fácil de entender con el ejemplo.

Covarianza

Por ejemplo, quieres comprar una flor y tienes dos florería en tu ciudad: la tienda de rosas y la tienda de margaritas.

Si le preguntas a alguien "¿dónde está la florería?" y alguien te dice dónde está la tienda de rosas, ¿estaría bien? sí, porque la rosa es una flor, si quieres comprar una flor puedes comprar una rosa. Lo mismo se aplica si alguien te responde con la dirección de la tienda de margaritas. Este es un ejemplo de covarianza : se le permite convertir A<C>a A<B>, donde Ces una subclase de B, si Aproduce valores genéricos (devuelve como resultado de la función). La covarianza se trata de productores.

Tipos:

class Flower {  }
class Rose extends Flower { }
class Daisy extends Flower { }

interface FlowerShop<T extends Flower> {
    T getFlower();
}

class RoseShop implements FlowerShop<Rose> {
    @Override
    public Rose getFlower() {
        return new Rose();
    }
}

class DaisyShop implements FlowerShop<Daisy> {
    @Override
    public Daisy getFlower() {
        return new Daisy();
    }
}

La pregunta es "¿dónde está la floristería?", La respuesta es "tienda de rosas allí":

static FlowerShop<? extends Flower> tellMeShopAddress() {
    return new RoseShop();
}

Contravarianza

Por ejemplo, quieres regalar flores a tu novia. Si tu novia ama cualquier flor, ¿puedes considerarla como una persona que ama las rosas o como una persona que ama las margaritas? sí, porque si ama cualquier flor, amaría tanto la rosa como la margarita. Este es un ejemplo de la contravarianza : se le permite lanzar A<B>a A<C>, donde Ces la subclase de B, si Aconsume valor genérico. La contravarianza se trata de consumidores.

Tipos:

interface PrettyGirl<TFavouriteFlower extends Flower> {
    void takeGift(TFavouriteFlower flower);
}

class AnyFlowerLover implements PrettyGirl<Flower> {
    @Override
    public void takeGift(Flower flower) {
        System.out.println("I like all flowers!");
    }

}

Estás considerando a tu novia que ama cualquier flor como alguien que ama las rosas, y le estás regalando una rosa:

PrettyGirl<? super Rose> girlfriend = new AnyFlowerLover();
girlfriend.takeGift(new Rose());

Puede encontrar más en la Fuente .

VadzimV
fuente
@Peter, gracias, es un buen punto. La invariancia es cuando no hay relaciones entre clases con diferentes parámetros genéricos, es decir, no se puede convertir A <B> a A <C> cualquiera que sea la relación entre B y C.
VadzimV