Nei moderni linguaggi di programmazione orientati agli oggetti tipo Java, C#, C++ e così via, che offrono il supporto sia del polimorfismo per inclusione (espresso dall’ereditarietà) sia del polimorfismo parametrico (espresso dai generici), esiste un problema importante legato all’assenza, di default, di una “sinergia comportamentale” tra i citati sistemi polimorfi.
In sostanza, quanto detto significa che, per il polimorfismo per inclusione, data una classe Base
e una classe Derived
da essa derivata, sarà sempre lecito assegnare un’istanza di tipo Derived
in una variabile di tipo Base
e ciò perché esiste sempre una relazione di sottotipo Derived <: Base
(il simbolo <:
denota tale relazione).
Nel contempo, per il polimorfismo parametrico, data una classe generica Generic<T>
e le classi parametrizzate Generic<Base>
e Generic<Derived>
non sarà mai lecito assegnare un’istanza di tipo Generic<Derived>
in una variabile di tipo Generic<Base>
e ciò perché non esiste mai una relazione di sottotipo Generic<Derived> <: Generic<Base>
.
Ciò detto, appare evidente che in un linguaggio di programmazione orientato agli oggetti, moderno e ben progettato, debba essere presente un meccanismo che consenta di esprimere una relazione “sinergica” di sottotipo, detta varianza, tra i tipi complessi (per esempio classi generiche e dunque proprie del polimorfismo parametrico) e i tipi utilizzati per costruirli (per esempio i tipi forniti per gli argomenti di tipo e dunque propri del polimorfismo per inclusione).
Date, quindi, le classi Generic<T>, Base
e Derived
, potremmo avere i seguenti casi.
- La relazione di sottotipo espressa da
Generic<Base>
eGeneric<Derived>
è preservata rispetto a quella espressa daBase
eDerived
, ossia è lecito e type safe assegnare un’istanza di tipoGeneric<Derived>
in una variabile di tipoGeneric<Base>
. In modo più formale, data una classe genericaC<T>
, essa è definita covariante rispetto aT
se la relazione di sottotipo tra le classiD <: B
implica la stessa relazione di sottotipo tra le classiC<D> <: C<B>
. - La relazione di sottotipo espressa da
Generic<Base>
eGeneric<Derived>
è invertita rispetto a quella espressa daBase
eDerived
, ossia è lecito e type safe assegnare un’istanza di tipoGeneric<Base>
in una variabile di tipoGeneric<Derived>
. In modo più formale, data una classe genericaC<T>
, essa è definita controvariante rispetto aT
se la relazione di sottotipo tra le classiD <: B
implica una relazione di sottotipo tra le classiC<B> <: C<D>
. - La relazione di sottotipo espressa da
Generic<Base>
eGeneric<Derived>
è ignorata rispetto a quella espressa daBase
eDerived
, ossia non è lecito e type safe produrre degli assegnamenti tra un tipoGeneric<Base>
e un tipoGeneric<Derived>
. In modo più formale, data una classe genericaC<T>
, essa è definita invariante rispetto aT
solo quando la relazione di sottotipo tra le classiC<D> <: C<B>
è valida perD = B
.
In definitiva l’espressione di una varianza consente di variare tra dei tipi complessi, per esempio dei tipi generici, la loro relazione di sottotipo in modo covariante (va nella stessa direzione) o controvariante (va in direzione opposta) rispetto alla relazione di sottotipo dei loro tipi costituenti, per esempio gli argomenti di tipo forniti per la costruzione di un tipo effettivo. Se non c’è alcuna variazione, allora i tipi complessi rimarranno invarianti, ossia non varieranno la loro relazione di sottotipo rispetto a quella dei loro tipi costituenti.
Per quanto attiene al linguaggio Java, di default tutti i tipi generici sono invarianti, ma è comunque possibile esprimere sugli argomenti di tipo la loro varianza, ossia definire se essi sono covarianti o controvarianti.
TERMINOLOGIA | Java consente di specificare la varianza su un argomento di tipo ossia quando è utilizzato un tipo generico (use-site variance annotation). Altri linguaggi consentono di specificare la varianza su un parametro di tipo, ossia durante la dichiarazione; per esempio, per C#, ciò è possibile nel caso di un’interfaccia o delegato generico (declaration-site variance annotation).
Lo snippet che segue mostra perché di default i tipi generici del linguaggio Java sono invarianti, mostrando cosa accadrebbe in caso contrario (il sistema sarebbe non type safe).
...
abstract class Dog { }
class WhiteTerrier extends Dog { }
class GoldenRetriever extends Dog { }
public class Snippet_9_7
{
public static void main(String[] args)
{
List<WhiteTerrier> wt = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
// di default non esiste nessuna varianza tra delle classi generiche
// non è cioè mai possibile creare una relazione di sottotipo tra due
// differenti istanze della stessa classe generica
// in fondo una lista di WhiteTerrier è un oggetto completamente diverso
// da una lista di Dog
dogs = wt; // error: incompatible types:
// List<WhiteTerrier> cannot be converted to List<Dog>
// se fosse lecito l'assegnamento dogs = wt sarebbe possibile mettere a
// compile time e a runtime un oggetto di tipo WhiteTerrier in una lista di
// WhiteTerrier
dogs.add(new WhiteTerrier());
// dal punto di vista del compilatore non c'è alcun problema nell'aggiungere
// nella lista di cani un golden retriever perché esiste una relazione di
// sottotipo tra GoldenRetriever e Dog ossia GoldenRetriever <: Dog
// in fondo, cioè, un golden retriever "è un" cane...
// tuttavia se fosse permesso l'assegnamento dogs = wt avremmo che a runtime
// dogs conterrebbe un riferimento di tipo WhiteTerrier ossia quella lista
// dovrebbe contenere solo oggetti di quel tipo, ma poi con la seguente
// istruzione aggiungiamo un oggetto di tipo GoldenRetriever che non è dunque
// compatibile e il sistema genererebbe un'eccezione di tipo ClassCastException
// ecco quindi che per ragioni di type safety
// il compilatore Java non permette di avere tipi generici varianti
dogs.add(new GoldenRetriever());
}
}
ATTENZIONE | Questo esempio è volutamente errato. Serve solo per mostrare l’effetto della varianza senza controlli a compile time sui tipi generici (in questo caso della covarianza).
Annotazioni di varianza e wildcard
Java consente di “aggirare” la limitazione descritta precedentemente di mancanza di relazione di ereditarietà tra i tipi generici (invarianza) consentendo di specificare, durante l’utilizzo di un tipo generico, per ogni argomento di tipo, una cosiddetta annotazione di varianza, ossia un’annotazione che indicherà se il relativo parametro di tipo sarà covariante oppure controvariante.
// Annotazione di covarianza: wildcard upper bound.
<? extends bound_type>
Si utilizzano tra le parentesi angolari:
- il carattere jolly (
?
) detto wildcard che rappresenta un tipo “sconosciuto”; - la keyword
extends
; - un tipo che indica un upper bound ossia un vincolo o limite superiore per il tipo che è possibile utilizzare durante la creazione di un tipo parametrizzato.
In pratica, dato un tipo T
, <? extends T>
indica che sarà possibile utilizzare qualsiasi sottotipo di T
oppure T
stesso.
La sintassi di covarianza permette dunque di esplicitare un’annotazione di covarianza con cui, in altre parole, dato un tipo List<S>
potremo sempre legittimamente assegnare un oggetto del suo tipo a una variabile di tipo List<? extends T>
se S
è di tipo T
o un sottotipo di T
.
// Annotazione di controvarianza: wildcard lower bound.
<? super bound_type>
Si utilizzano tra le parentesi angolari:
- il carattere jolly (
?
) detto wildcard che rappresenta un tipo “sconosciuto”; - la keyword
super
; - un tipo che indica un lower bound, ossia un vincolo o limite inferiore per il tipo che è possibile utilizzare durante la creazione di un tipo parametrizzato.
In pratica, dato un tipo T
, <? super T>
indica che sarà possibile utilizzare qualsiasi supertipo di T
oppure T
stesso.
La sintassi di controvarianza permette dunque di esplicitare un’annotazione di controvarianza con cui, in altre parole, dato un tipo List<S>
potremo sempre legittimamente assegnare un oggetto del suo tipo a una variabile di tipo List<? super T>
se S
è di tipo T
o un supertipo di T
.
NOTA | È possibile usare anche la seguente annotazione di varianza
<?>
, definita come unbounded wildcard (wildcard senza vincoli), che è uno shortcut equivalente all’annotazione<? extends Object>
con cui si esplicita un qualsiasi tipo il cui limite superiore è il tipoObject
.ATTENZIONE | Il carattere
?
non si può usare nella sezione dei parametri di tipo e anche come mero identificatore di tipo (per esempio, per dichiarare una variabile locale a un metodo o un campo di una classe).
// Annotazioni di varianza.
...
public class Snippet_9_8
{
public static void main(String[] args)
{
// dichiarazione COVARIANTE
// ? potrà essere di un tipo derivato da Number oppure Number stesso
// l'unica operazione type safe sarà però la "lettura"
// numbers potrà avere come tipo dei suoi elementi qualsiasi tipo derivato da Number
// o Number stesso
// sarà pertanto sempre lecito assegnare un elemento di numbers,
// ora di tipo Integer, poi di tipo Double, a number di tipo Number
// in altre parole: il get di un elemento di numbers sarà sempre sicuro
// perché, qualsiasi sarà il tipo, esso sarà sempre assegnabile con
// il tipo Number, unico tipo conosciuto con certezza dal compilatore
// un tipo parametrizzato covariante è dunque un tipo read-only
// tipicamente, infatti, i valori restituiti utilizzano wildcard vincolati
// superiormente ovvero la covarianza è generalmente associata a costrutti che
// "producono" o restituiscono dei valori
List<? extends Number> numbers = Arrays.asList(new Integer[] { 1, 2, 3, 4, 5 });
Number number = numbers.get(0); // 1
// non è possibile usare un tipo parametrizzato covariante in "scrittura" perché
// altrimenti si violerebbe la type safety
// infatti, dato che ? esprime un tipo sconosciuto il compilatore non ne conosce
// la sua natura e pertanto non potrà mai permettere inserimenti nella lista
// ? sarà un Integer?, sarà un Number?, non si sa...
// error: incompatible types...
numbers.add(1, 11.2);
numbers = Arrays.asList(new Double[] { 1.1, 2.2, 3.3, 4.4, 5.5 });
number = numbers.get(0); // 1.1
// dichiarazione CONTROVARIANTE
// ? potrà essere Integer stesso oppure un suo supertipo come Number
// l'unica operazione type safe sarà però la "scrittura"
// other_numbers potrà avere come tipo dei suoi elementi Integer stesso
// sarà pertanto sempre lecito aggiungere in numbers un elemento di tipo Integer
// in altre parole: l'add di un elemento in numbers sarà sempre sicuro
// perché, il tipo Integer, unico tipo conosciuto con certezza dal compilatore
// sarà sempre inseribile in una lista di Integer oppure di Number
// un tipo parametrizzato controvariante è dunque un tipo write-only
// tipicamente, infatti, i parametri utilizzano wildcard vincolati inferiormente
// ovvero la controvarianza è generalmente associata a costrutti che "consumano"
// o prendono come argomenti dei valori
List<? super Integer> other_numbers = new ArrayList<Number>();
// quest'aggiunta, ribadiamo, è type safe perché un tipo Integer
// è sempre inseribile in una lista di Integer oppure, come nel nostro caso,
// in una lista di tipo Number
other_numbers.add(11);
// non è possibile usare un tipo parametrizzato controvariante in "lettura" perché
// altrimenti si violerebbe la type safety
// infatti, dato che ? esprime un tipo sconosciuto il compilatore non ne conosce
// la sua natura e pertanto non potrà mai permettere estrazioni dalla lista
// ? sarà un Integer?, sarà un Number?, non si sa...
// error: incompatible types...
number = other_numbers.get(0);
// dichiarazione INVARIANTE
// di default i tipi generici sono invarianti e dunque la sottoindicata statement
// non è ammessa
// error: incompatible types: ArrayList<Integer>
// cannot be converted to List<Number>
List<Number> again_numbers = new ArrayList<Integer>();
}
}
Mostriamo ora un esempio più complesso che evidenzia ulteriormente l’importanza delle annotazioni di varianza esprimibili attraverso le già citate wildcard.
// VarianceAndWildcards.java (VarianceAndWildcards).
package LibroJava11.Capitolo9;
import java.util.ArrayList;
interface IEnumerate<T>
{
T getElement(int ix);
}
interface ICompare<T>
{
boolean bornBefore(T t1, T t2);
}
abstract class Dog
{
protected String name;
protected int age;
public int getAge() { return age; }
public String getName() { return name; }
}
class WhiteTerrier extends Dog
{
public WhiteTerrier(String name, int age)
{
this.name = name;
this.age = age;
}
}
class GoldenRetriever extends Dog
{
public GoldenRetriever(String name, int age)
{
this.name = name;
this.age = age;
}
}
class DogComparer implements ICompare<Dog>
{
public boolean bornBefore(Dog t1, Dog t2)
{
return t1.getAge() < t2.getAge();
}
}
class MyList<T> extends ArrayList<T> implements IEnumerate<T>
{
public void addElement(T el)
{
add(el);
}
public T getElement(int ix)
{
return get(ix);
}
}
public class VarianceAndWildcards
{
public static void main(String[] args) throws Exception
{
// DIMOSTRAZIONE DELLA CONTROVARIANZA -----------------------------------
//
// Ok - assegnamento lecito un DogComparer "è un" ICompare<Dog> perché
// l'ha in effetti implementato
ICompare<Dog> dc = new DogComparer();
WhiteTerrier wt = new WhiteTerrier("Winter", 1);
WhiteTerrier ot = new WhiteTerrier("Sammy", 4);
GoldenRetriever gr = new GoldenRetriever("Sandy", 10);
GoldenRetriever or = new GoldenRetriever("Joel", 14);
// OK - un comparatore di Dog può comparare WhiteTerrier e GoldenRetriever
// perché entrambi "sono" dei Dog
System.out.printf("%s è nato prima di %s? [%b]%n", wt.getName(), gr.getName(),
dc.bornBefore(wt, gr));
System.out.printf("%s è nato prima di %s? [%b]%n", wt.getName(), ot.getName(),
dc.bornBefore(wt, ot));
// questa conversione è lecita, perché rendiamo tc controvariante
// in effetti si sta assegnando dc che è di tipo ICompare<Dog>
// in tc che è di tipo ICompare<? super WhiteTerrier> e dunque la relazione
// di sottotipo è invertita
// ? potrà essere di tipo WhiteTerrier oppure un suo supertipo come Dog
ICompare<? super WhiteTerrier> tc = dc;
// un comparatore di Dog è sicuramente in grado di comparare dei WhiteTerrier
// esso è infatti utilizzabile in modo "più generale" rispetto ai suoi casi
// "più specifici" e dunque fornisce una certa flessibilità di impiego
// l'invocazione del metodo è type safe e il compilatore non permetterà di
// comparare oggetti di tipo diverso da WhiteTerrier
tc.bornBefore(wt, ot);
// anche qui la conversione è lecita... così come la comparazione
ICompare<? super GoldenRetriever> gc = dc;
gc.bornBefore(gr, or);
// DIMOSTRAZIONE DELLA COVARIANZA -------------------------------------------
//
// creo una lista di tipo MyList<WhiteTerrier> che implementa un'interfaccia
// di tipo IEnumerate<WhiteTerrier>
MyList<WhiteTerrier> wtl = new MyList<>();
wtl.addElement(new WhiteTerrier("Luc", 3));
wtl.addElement(new WhiteTerrier("Ric", 5));
// è lecito assegnare wtl a eg perché di fatto eg "è un" IEnumerate<? extends Dog>
// covariante e quindi come è valida tale relazione di sottotipo
// WhiteTerrier <: Dog così è valida per
// IEnumerate<WhiteTerrier> <: IEnumerate<? extends Dog>
// ? potrà essere di tipo Dog oppure un suo sottotipo come WhiteTerrier
IEnumerate<? extends Dog> eg = wtl;
// è type safe ottenere l'elemento 0 di eg perché è, in effetti, di tipo
// WhiteTerrier e può dunque essere assegnato ad a_dog di tipo Dog
Dog a_dog = eg.getElement(0);
System.out.println(a_dog.getName());
// creo una lista di tipo MyList<GoldenRetriever> che implementa
// un'interfaccia di tipo IEnumerate<GoldenRetriever>
MyList<GoldenRetriever> grl = new MyList<>();
grl.addElement(new GoldenRetriever("Andy", 4));
grl.addElement(new GoldenRetriever("Puppy", 9));
// è lecito riutilizzare IEnumerate<? extends Dog> che ora riferirà un
// IEnumerate<GoldenRetriever> sempre possibile per effetto della covarianza
eg = grl;
a_dog = eg.getElement(0);
System.out.println(a_dog.getName());
}
}
// Output dal listato VarianceAndWildcards.java.
Winter è nato prima di Sandy? [true]
Winter è nato prima di Sammy? [true]
Luc
Andy
Il Listato VarianceAndWildcards.java
definisce:
- le interfacce
IEnumerate<T>
eICompare<T>
; - una gerarchia di classi costituita dalla classe base
Dog
, da cui derivano la classeWhiteTerrier
e la classeGoldenRetriever
; - la classe
DogComparer
che implementa l’interfacciaICompare<Dog>
; saranno usate dal metodomain
per dimostrare il funzionamento della controvarianza; - la classe
MyList<T>
, che deriva dalla classejava.util.ArrayList<T>
e implementa l’interfacciaIEnumerate<T>
; saranno usate dal metodomain
per dimostrare il funzionamento della covarianza.
Per quanto riguarda la controvarianza, dc
è di fatto un comparatore di Dog
e infatti notiamo come sia subito possibile comparare wt
e gr
di tipo WhiteTerrier
e GoldenRetriever
e poi ancora wt
e ot
entrambi di tipo WhiteTerrier
; il metodo bornBefore
, infatti, ha come tipo dei suoi parametri il tipo Dog
, e dunque, per la relazione is-a, è lecito assegnare un WhiteTerrier
o un GoldenRetriever
a un tipo Dog
. Dopodiché, e questo è il punto fondamentale, assegniamo dc
(che è di tipo ICompare<Dog>
) alla variabile tc
(che è di tipo ICompare<? super WhiteTerrier>
) e questo è possibile perché ICompare
è stata parametrizzata utilizzando un’apposita annotazione di controvarianza. Infatti, come si nota, il verso della relazione di sottotipo è opposto rispetto a quanto esplicitato tra Dog
e WhiteTerrier
.
Infine tramite tc
invochiamo ancora il metodo bornBefore
, che funzionerà senza problemi perché a runtime tc
punterà a un’istanza di tipo DogComparer
, che ha implementato l’interfaccia ICompare<Dog>
, la quale “saprà” di sicuro come comparare gli oggetti di tipo WhiteTerrier
passati come argomenti. Lo stesso avverrà e sarà possibile tramite gc
di tipo ICompare<? super GoldenRetriever>
.
NOTA | Possiamo provare a spiegare anche in altro modo il perché nel contesto dell’assegnamento di
dc
atc
(ogc
) tutto funziona in modo type safe: a compile time il compilatore non genererà alcun messaggio d’errore, perché gli abbiamo assicurato che l’interfacciaICompare
è controvariante; a runtime la JVM non genererà alcuna eccezione, perchétc
(ogc
) faranno riferimento a un oggetto di tipoDogComparer
, il cui metodobornBefore
avrà parametri di tipoDog
che potranno quindi accettare senza problemi gli argomenti di tipoWhiteTerrier
eGoldenRetriever
.
Per quanto riguarda la covarianza, notiamo come l’assegnamento di wtl
, che è di tipo MyList<WhiteTerrier>
ma è anche un tipo IEnumerate<WhiteTerrier>
, sia possibile nella variabile eg
di tipo IEnumerate<? extends Dog>
e questo perché l’interfaccia IEnumerate
è stata parametrizzata utilizzando un’apposita annotazione di covarianza; infatti, come si nota, il verso della relazione di sottotipo è lo stesso rispetto a quanto esplicitato tra Dog
e WhiteTerrier
.
Dopodiché tramite eg
invochiamo senza problemi il metodo getElement
che restituirà il primo oggetto della lista che sarà di tipo WhiteTerrier
e dunque legittimamente assegnabile alla variabile a_dog
di tipo Dog
(eg
a runtime punterà infatti a una lista di tipo MyList<WhiteTerrier>
che è anche di tipo IEnumerate<WhiteTerrier>
).
La stessa logica e modalità di funzionamento varrà anche quando assegneremo nella variabile eg
un’istanza di tipo MyList<GoldenRetriever>
(che è un IEnumerate<GoldenRetriever>
).
Covarianza degli array
Se abbiamo una classe B
dalla quale deriva una classe D
, allora possiamo asserire che, dato che esiste una compatibilità di assegnamento (assignment compatibility) tra D
e B
, ossia è sempre possibile assegnare un oggetto di un tipo D
(un tipo derivato, ossia un tipo più specializzato, a narrower type) a una variabile di tipo B
(un tipo base, ossia un tipo meno specializzato, a wider type), avremo che allo stesso tempo esiste una compatibilità di assegnamento tra un array di oggetti tipo D
e un array di oggetti di tipo B
.
In modo più generalizzato, possiamo dunque dire che, dati due tipi riferimento N
e M
, se tra essi esiste una conversione implicita o esplicita, allora la stessa conversione esiste tra gli array N[]
e M[]
. Questa relazione rappresenta, dunque, una relazione di covarianza degli array.
Anche se la covarianza degli array è di default integrata nel linguaggio Java, bisogna prestare attenzione all’utilizzo di una variabile di un tipo array soprattutto quando essa è di un tipo base e può quindi accettare in assegnamento array di tutti i suoi tipi derivati.
In pratica, come dimostra il seguente snippet, la covarianza degli array non garantisce a compile time una sicurezza sui tipi (non è cioè type safe) e ciò può causare a runtime un serio problema di un’eccezione software di tipo ArrayStoreException
, la quale viene generata, per l’appunto, quando si tenta di inserire in un array un elemento di un tipo errato.
// Covarianza degli array.
...
abstract class Dog
{
protected String name;
protected int age;
public int getAge() { return age; }
public String getName() { return name; }
}
class WhiteTerrier extends Dog
{
public WhiteTerrier(String name, int age)
{
this.name = name;
this.age = age;
}
}
class GoldenRetriever extends Dog
{
public GoldenRetriever(String name, int age)
{
this.name = name;
this.age = age;
}
}
public class Snippet_9_9
{
public static void main(String[] args)
{
// OK - array covariance
// un array di Dog può contenere un riferimento verso un array di
// WhiteTerrier perché un WhiteTerrier "è un" Dog
Dog[] dogs = new WhiteTerrier[2];
dogs[0] = new WhiteTerrier("Winter", 1); // OK a compile time e a runtime
// possiamo assegnare a compile time un tipo GoldenRetriever come elemento di un
// array di tipo Dog perché un GoldenRetriever "è un" Dog ma a runtime questo
// genererà un'eccezione, perché, di fatto, dogs contiene un riferimento
// verso un array di tipo WhiteTerrier e il sistema ci dice che stiamo
// tentando di assegnare in modo illecito all'elemento 1 dell'array di tipo
// WhiteTerrier un elemento di tipo GoldenRetriever (in pratica stiamo provando
// a mettere in un "cesto di mele anche delle pere...")
dogs[1] = new GoldenRetriever("Sandy", 11); // OK a compile time ma NON a runtime
// Eccezione di tipo:
// java.lang.ArrayStoreException
}
}
Questa figura evidenzia quanto detto: se il sistema di runtime non generasse un’apposita eccezione software, si permetterebbe di memorizzare in un array di tipo WhiteTerrier
un elemento di tipo GoldenRetriever
e questo non è ammissibile.
IMPORTANTE | È bene non usare gli array in modo covariante per le ragioni di mancanza di type safety ora evidenziate. Nel caso si può utilizzare l’interfaccia
java.lang.Iterable<T>
che non permette, in breve, alcuna operazione di inserimento. Si può creare, per esempio, un arraywt
di tipoWhiteTerrier
, popolarlo con i suoi elementi e poi assegnarne il riferimento a una variabile di tipoIterable<Dog>
tramite un costrutto come il seguente:Iterable<Dog> dogs = Arrays.asList(wt)
.