Covarianza, invarianza e controvarianza spiegate in un inglese semplice?


113

Oggi ho letto alcuni articoli su Covarianza, Contravarianza (e Invarianza) in Java. Ho letto l'articolo di Wikipedia in inglese e tedesco e altri post di blog e articoli di IBM.

Ma sono ancora un po 'confuso su di cosa si tratta esattamente? Alcuni dicono che si tratta di relazione tra tipi e sottotipi, alcuni dicono che si tratta di conversione di tipi e alcuni dicono che sia usato per decidere se un metodo è sovrascritto o sovraccarico.

Quindi sto cercando una spiegazione semplice in un inglese semplice, che mostri a un principiante cosa sono Covarianza e Contravarianza (e Invarianza). Punto in più per un semplice esempio.


Fare riferimento a questo post, potrebbe esserti utile: stackoverflow.com/q/2501023/218717
Francisco Alvarado

3
Forse meglio la domanda del tipo di scambio di stack di un programmatore. Se pubblichi lì, considera di affermare solo ciò che capisci e cosa specificamente ti confonde, perché in questo momento stai chiedendo a qualcuno di riscrivere un intero tutorial per te.
Hovercraft Full Of Eels

Risposte:


288

Alcuni dicono che si tratta di relazione tra tipi e sottotipi, altri dicono che si tratta di conversione del tipo e altri dicono che è usato per decidere se un metodo viene sovrascritto o sovraccaricato.

Tutti i precedenti.

In sostanza, questi termini descrivono in che modo la relazione del sottotipo è influenzata dalle trasformazioni di tipo. Cioè, se Ae Bsono tipi, fè una trasformazione di tipo e ≤ la relazione del sottotipo (cioè A ≤ Bsignifica che Aè un sottotipo di B), abbiamo

  • fè covariante se lo A ≤ Bimplicaf(A) ≤ f(B)
  • fè controvariante se lo A ≤ Bimplicaf(B) ≤ f(A)
  • f è invariante se nessuna delle precedenti è valida

Consideriamo un esempio. Lasciate f(A) = List<A>dove Listè dichiarato da

class List<T> { ... } 

È fcovariante, controvariante o invariante? Covariante significherebbe che una List<String>è un sottotipo di List<Object>, controvariante che List<Object>è un sottotipo di List<String>ed invariante che né è un sottotipo dell'altro, cioè List<String>e List<Object>sono tipi inconvertibili. In Java, quest'ultimo è vero, diciamo (un po 'informalmente) che i generici sono invarianti.

Un altro esempio. Let f(A) = A[]. È fcovariante, controvariante o invariante? Cioè, String [] è un sottotipo di Object [], Object [] un sottotipo di String [], o non è né un sottotipo dell'altro? (Risposta: in Java, gli array sono covarianti)

Questo era ancora piuttosto astratto. Per renderlo più concreto, diamo un'occhiata a quali operazioni in Java sono definite in termini di relazione del sottotipo. L'esempio più semplice è l'assegnazione. La dichiarazione

x = y;

compilerà solo se typeof(y) ≤ typeof(x). Cioè, abbiamo appena appreso che le dichiarazioni

ArrayList<String> strings = new ArrayList<Object>();
ArrayList<Object> objects = new ArrayList<String>();

non verrà compilato in Java, ma

Object[] objects = new String[1];

volere.

Un altro esempio in cui la relazione del sottotipo è importante è un'espressione di invocazione del metodo:

result = method(a);

In modo informale, questa istruzione viene valutata assegnando il valore di aal primo parametro del metodo, quindi eseguendo il corpo del metodo e quindi assegnando il valore restituito ai metodi result. Come l'assegnazione semplice nell'ultimo esempio, il "lato destro" deve essere un sottotipo del "lato sinistro", ovvero questa affermazione può essere valida solo se typeof(a) ≤ typeof(parameter(method))e returntype(method) ≤ typeof(result). Cioè, se il metodo è dichiarato da:

Number[] method(ArrayList<Number> list) { ... }

nessuna delle seguenti espressioni verrà compilata:

Integer[] result = method(new ArrayList<Integer>());
Number[] result = method(new ArrayList<Integer>());
Object[] result = method(new ArrayList<Object>());

ma

Number[] result = method(new ArrayList<Number>());
Object[] result = method(new ArrayList<Number>());

volere.

Un altro esempio in cui la sottotipizzazione è importante. Tener conto di:

Super sup = new Sub();
Number n = sup.method(1);

dove

class Super {
    Number method(Number n) { ... }
}

class Sub extends Super {
    @Override 
    Number method(Number n);
}

In modo informale, il runtime lo riscriverà in:

class Super {
    Number method(Number n) {
        if (this instanceof Sub) {
            return ((Sub) this).method(n);  // *
        } else {
            ... 
        }
    }
}

Per la compilazione della riga contrassegnata, il parametro del metodo del metodo sovrascritto deve essere un supertipo del parametro del metodo del metodo sovrascritto e il tipo restituito un sottotipo di quello del metodo sostituito. Formalmente parlando, f(A) = parametertype(method asdeclaredin(A))deve almeno essere controvariante e se f(A) = returntype(method asdeclaredin(A))deve essere almeno covariante.

Notare "almeno" sopra. Questi sono requisiti minimi che qualsiasi linguaggio di programmazione orientato agli oggetti ragionevole e staticamente sicuro imporrà, ma un linguaggio di programmazione può scegliere di essere più rigoroso. Nel caso di Java 1.4, i tipi di parametro ei tipi di restituzione del metodo devono essere identici (eccetto per la cancellazione del tipo) quando si sovrascrivono i metodi, cioè parametertype(method asdeclaredin(A)) = parametertype(method asdeclaredin(B))quando si sovrascrive. A partire da Java 1.5, i tipi restituiti covarianti sono consentiti durante l'override, ovvero quanto segue verrà compilato in Java 1.5, ma non in Java 1.4:

class Collection {
    Iterator iterator() { ... }
}

class List extends Collection {
    @Override 
    ListIterator iterator() { ... }
}

Spero di aver coperto tutto, o meglio, di aver graffiato la superficie. Tuttavia spero che possa aiutare a capire il concetto astratto ma importante della varianza di tipo.


1
Inoltre, poiché i tipi di argomenti controvarianti di Java 1.5 sono consentiti durante l'override. Penso che ti sia mancato.
Brian Gordon

13
Sono loro? L'ho appena provato in Eclipse, e il compilatore ha pensato che intendessi sovraccaricare piuttosto che sovrascrivere e ha rifiutato il codice quando ho inserito un'annotazione @Override sul metodo della sottoclasse. Hai qualche prova per la tua affermazione che Java supporta i tipi di argomenti controvarianti?
meriton

1
Ah, hai ragione. Ho creduto a qualcuno senza controllarlo di persona.
Brian Gordon

1
Ho letto molta documentazione e guardato alcuni discorsi su questo argomento, ma questa è di gran lunga la migliore spiegazione. Grazie mille.
minzchickenflavor

1
+1 per essere assolutamente leman e semplice con A ≤ B. Questa notazione rende le cose molto più semplici e significative. Buona lettura ...
Romeo Sierra

12

Prendendo il sistema di tipo java e quindi le classi:

Qualsiasi oggetto di qualche tipo T può essere sostituito con un oggetto di sottotipo di T.

VARIANZA DI TIPO - I METODI DI CLASSE HANNO LE SEGUENTI CONSEGUENZE

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;

Si può vedere che:

  • La T deve essere il sottotipo S ( covariante, poiché B è il sottotipo di A ).
  • La V deve essere il supertipo di U ( controvariante , come contro la direzione dell'ereditarietà).

Ora co- e contro-correlano al fatto che B sia sottotipo di A. Le seguenti tipizzazioni più forti possono essere introdotte con una conoscenza più specifica. Nel sottotipo.

La covarianza (disponibile in Java) è utile, per dire che si restituisce un risultato più specifico nel sottotipo; visto soprattutto quando A = T e B = S. La controvarianza dice che sei pronto a gestire un argomento più generale.


8

La varianza riguarda le relazioni tra classi con diversi parametri generici. Le loro relazioni sono il motivo per cui possiamo lanciarli.

La varianza di Co e Contra sono cose abbastanza logiche. Il sistema dei tipi di linguaggio ci costringe a supportare la logica della vita reale. È facile da capire con l'esempio.

covarianza

Ad esempio, vuoi comprare un fiore e hai due negozi di fiori nella tua città: negozio di rose e negozio di margherite.

Se chiedi a qualcuno "dov'è il negozio di fiori?" e qualcuno ti dice dov'è il negozio di rose, andrebbe bene? si perché la rosa è un fiore, se vuoi comprare un fiore puoi comprare una rosa. Lo stesso vale se qualcuno ti ha risposto con l'indirizzo del negozio margherita. Questo è un esempio di covarianza : è consentito eseguire il cast A<C>a A<B>, dove Cè una sottoclasse di B, se Aproduce valori generici (restituisce come risultato della funzione). La covarianza riguarda i produttori.

tipi:

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 domanda è "dov'è il negozio di fiori?", La risposta è "negozio di rose lì":

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

controvarianza

Ad esempio, vuoi regalare un fiore alla tua ragazza. Se la tua ragazza ama qualsiasi fiore, puoi considerarla una persona che ama le rose o una persona che ama le margherite? sì, perché se ama qualsiasi fiore amerebbe sia la rosa che la margherita. Questo è un esempio della controvarianza : ti è consentito eseguire il cast A<B>a A<C>, dov'è la Csottoclasse di B, se Aconsuma un valore generico. La controvarianza riguarda i consumatori.

tipi:

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!");
    }

}

Stai considerando la tua ragazza che ama qualsiasi fiore come qualcuno che ama le rose e le dai una rosa:

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

Puoi trovare di più alla Fonte .


@Peter, grazie, è un punto giusto. L'invarianza è quando non ci sono relazioni tra classi con diversi parametri generici, cioè non puoi lanciare A <B> in A <C> qualunque sia la relazione tra B e C.
VadzimV
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.