Tipi generici e relazioni di sottotipo

Se S $\prec$ T non è vero che ≠ G<S> $\prec$ G<T> (dove G è un qualche tipo generico). Usando una immagine tratta dal tutorial

Si considerino ad esempio

In [1]:
List<Integer> li = new ArrayList<>();
List<Object> lo;

Se li fosse un sottotipo di lo l'assegnamento seguente sarebbe possibile (senza cast)

In [2]:
lo = (List)li;

Una volta ottenuto l'alias lo potremmo usarlo per aggiungere di tutto a li

In [3]:
lo.add(new Object());
Out[3]:
true

ma ovviamente, una volta estratto da li (come Integer) un oggetto qualunque potrebbe dare adito ad un errore di conversione

In [4]:
li.get(0);
---------------------------------------------------------------------------
java.lang.ClassCastException: class java.lang.Object cannot be cast to class java.lang.Integer (java.lang.Object and java.lang.Integer are in module java.base of loader 'bootstrap')
	at .(#16:1)

Abbiamo già visto che il punto, in questo caso, è poter aggiungere e togliere elementi del tipo parametrico (avendo sia la garnzia della type safety che evitando l'uso del cast).

In [5]:
li.add(Integer.valueOf(1));
Integer i = li.get(1);

Inoltre, come già discusso, vale invece che se G $\prec$ H, allora per ogni T vale che G<T> $\prec$ H<T>, di nuovo usando una immagine dal tutorial

Array e gerarchia

Si osservi che la situazione è ben diversa per gli array: è infatti vero che se S $\prec$ T, allora S[] $\prec$ T[].

In [6]:
Integer[] ai = new Integer[10];
Object[] ao;

In questo caso, infatti, l'assegmaneto è possibile senza cast (per via della relazione di sottotipo)

In [7]:
ao = ai;

Il fatto è che, per gli array, l'assegnamento a ao può essere controllato a run-time perché l'array conserva l'informazione circa il tipo dei suoi elementi (cosa che non accade per i tipi generici).

In [8]:
ao[0] = new Object();
---------------------------------------------------------------------------
java.lang.ArrayStoreException: java.lang.Object
	at .(#22:1)

L'eccezione ArrayStoreException è proprio il modo in cui la VM segnala (a runtime) che è impossibile assegnare un oggetto ad un elemento di as (tramite l'alias ao).

Ovviamente è corretto l'assegnamento

In [9]:
ao[0] = Integer.valueOf(1);
Out[9]:
1

che viene eseguito senza errore.

Nel caso degli array quindi la type safety può essee garantita anche "trasportando" sugli array la relazione di sottotipo dgli elementi.

Metodi generici

La mancanza di relazioni di tipo tra G<S> e G<T> anche qualora S $\prec$ T è particolarmente grave non tanto per il caso degli alias visto in precendeza, quanto per il caso dei metodi.

Supponiamo di voler scrivere il metodo add al quale passeremo un parametro che "produca" dei valori numerici di cui il metodo restiturà la somma

In [10]:
public static double add(List<Number> lst) {
    double sum = 0;
    for (Number n : lst) sum += n.doubleValue();
    return sum;
}

Tutto bene se lo usiamo con una lista del tipo del parametro

In [11]:
List<Number> nums = List.of(1, 2.5, 3);
add(nums)
Out[11]:
6.5

Ma per via della mancanza della relazione di sottotipo, non possiamo usarla per sommare una lista di interi

In [12]:
List<Integer> ints = List.of(1, 2, 3);
add(ints);
|   add(ints);
incompatible types: java.util.List<java.lang.Integer> cannot be converted to java.util.List<java.lang.Number>

Una possibile soluzione è rendere il metodo generico ed indicare un bound nella dichiarazione dei parametri di tipo

In [13]:
public static <T extends Number> double add(List<T> lst) {
    double sum = 0;
    for (T n : lst) sum += n.doubleValue();
    return sum;
}
In [14]:
add(ints);
Out[14]:
6.0

Supponiamo ora di voler scrivere un metodo copy che data una lista proceda a copiare il suo contenuto in un "consumatore" costituito da una seconda lista

In [15]:
public static <T> void copy(List<T> src, List<T> dst) {
    dst.clear();
    for (T t : src) dst.add(t);
}

che funziona egregiamente su coppie di liste di interi

In [16]:
List<Integer> dupInts = new ArrayList<>();
copy(ints, dupInts);
dupInts
Out[16]:
[1, 2, 3]

Ma che succede se volessimo copiare una lista di interi in una di numeri?

In [17]:
List<Number> dupNums = new ArrayList<>();
copy(ints, dupNums);
|   copy(ints, dupNums);
method copy in class  cannot be applied to given types;
  required: java.util.List<T>,java.util.List<T>
  found:    java.util.List<java.lang.Integer>,java.util.List<java.lang.Number>
  reason: inference variable T has incompatible equality constraints java.lang.Number,java.lang.Integer

Incorreremmo di nuovo in un problema legato all'assenza di una relazione gerarchica tra i tipi; anche in questo caso, possiamo risolverlo con un bound

In [18]:
public static <T, S extends T> void copy(List<S> src, List<T> dst) {
    dst.clear();
    for (S t : src) dst.add(t);
}
In [19]:
copy(ints, dupNums);
dupNums
Out[19]:
[1, 2, 3]

L'uso dei bound sui tipi di parametro dei metodi generici è una prima risposta al problema della mancanza di una gerarchia tra i generici.

Ma è necessaria una soluzione più versatile, in grado di permetterci di dare un tipo (generico) sensato agli argomenti dei metodi.

Wildcard

Esiste un parametro di tipo wildcard ? che intuitivamente sta per "qualunque tipo", ragion per cui vale G<T> $\prec$G<?> (per ogni T); con un immagine tratta dal tutorial

Ad esempio

In [20]:
List<?> lw;

lw = lo;
lw = li;

Il problema è che in questo modo non è possibile garantire alcuna type safety, infatti non c'è verso di scrivere nella lista

In [21]:
lw.add(Integer.valueOf(1));
|   lw.add(Integer.valueOf(1));
incompatible types: java.lang.Integer cannot be converted to capture#2 of ?
In [22]:
lw.add(new Object());
|   lw.add(new Object());
incompatible types: java.lang.Object cannot be converted to capture#4 of ?

Anche la lettura non può essere fatta in modo type safe

In [23]:
Integer i = lw.get(1);
|   Integer i = lw.get(1);
incompatible types: capture#5 of ? cannot be converted to java.lang.Integer

Ma se l'obiettivo è solo recuperare un elemento, si può fare a patto di usare Object come tipo d'ultima istanza

In [24]:
Object o = lw.get(0);

Tale "libertà" può essere vincolata in vario modo, così che abbia senso usare tipi parametrici basati su wildcard.

Upper bound

Il caso più semplice è quello degli upper bound della forma ? extends T che consentono di introdurre le seguenti relazione di sottotipo (fissato G):

  • per ogni T, G<T> $\prec$ G<? extends T>
  • se S $\prec$ T, allora G<? extends S> $\prec$ G<? extends T>.

Ragionando per transitività, se S $\prec$ T, si ha G<S> $\prec$ G<? extends T> che, in somma, permette di concludere che il generico basato sull'upper bound del supertipo (G<? extends T>) è supertipo sia di G<S> che di G<T> (che pure sono tra loro inconfrontabili dal punto di vista della gerarchia).

Esempio: produttore

Un esempio d'uso può chiarire l'obiettivo di tali bound. Immaginiamo di avere un metodo in grado di operare su oggetti di tipo T prodotti da una lista; esso potrà riceverne una di tipo List<T> ma, certamente, anche List<S> (se S $\prec$ T); per questa ragione ha senso che il tipo del sua parametro sia List<? extends T>.

Ad esempio, consideriamo il metodo add visto in precedenza: a questo punto ha senso abbia un parametro di tipo List<? extends Number> che è supertipo di List<Integer> e List<Double>.

In [25]:
static double add(List<? extends Number> lst) {
    double sum = 0;
    for (Number n : lst) sum += n.doubleValue();
    return sum;
}
In [26]:
add(ints);
Out[26]:
6.0
In [27]:
add(nums);
Out[27]:
6.5

Il parametro del metodo add viene chiamato produttore perché produce i valori adoperati dal metodo, se esso è in grado di gestire il tipo Number sarà in grado di gestire i sottotipi. Il produttore, quindi, deve emettere elementi al più di un certo tipo, per questa ragione il suo parametro ha un upper bound.

Lower bound

Immaginiamo ora di avere un metodo in grado di consumare oggetti di tipo T da immagazzinare in una lista, che tipo dovrebbe avere quest'ultima? Non possiamo seguire il ragionamento precedente: volendo aggiungere valori di tipo T non possiamo farlo in una lista di tipo List<? extends T>, perché sappiamo che se S $\prec$ T ad essa può corrispondere anche una List<S> e finiremmo col mettere oggetti del supertipo in una lista di sottotipi! Vorremmo scrivere una cosa del tipo T extends ?, ma questo non è sintatticamente ammesso.

A tal fine vengono invece introdotti i lower bound della forma ? super S che consentono di introdurre le seguenti relazioni di sottotipo

  • per ogni T, G<T> $\prec$ G<? super T>
  • se S $\prec$ T, allora G<? super T> $\prec$ G<? super S>

si osservi che, nella seconda relazione, l'ordine dei generici è rovesciato rispetto a prima.

Ragionando per transitività, se S $\prec$ T, si ha G<T> $\prec$ G<? super S> che, in somma, permette di concludere che: il generico basato sul lower bound del sottotipo (G<? super S>) è supertipo sia di G<S> che di G<T>.

Esempio: consumatore

Ad esempio, consideriamo di nuovo il metodo copy visto in precedenza. L'unico vincolo in questo caso è che il consumatore di elementi (estratti dalla lista di T) sia una List<? super T> (che è supertipo di List<T>) in grado di ricevere elementi di tipo T.

In [28]:
static <T> void copy(List<T> src, List<? super T> dst) {
    dst.clear();
    for (T t : src) dst.add(t);
}
In [29]:
copy(ints, dupInts);
dupInts
Out[29]:
[1, 2, 3]
In [30]:
copy(ints, dupNums);
dupNums
Out[30]:
[1, 2, 3]

Il secondo parametro del metodo copy viene chiamato consumatore perché riceve o valori dalla (prima) lista.

Si osservi inoltre che con le wildcard è possibile anche restringere il tipo della prima lista, ad esempio ad Integer, in questo modo non è nemmeno necessario che il metodo sia generico

In [31]:
static void copy(List<Integer> src, List<? super Integer> dst) {
    dst.clear();
    for (Integer t : src) dst.add(t);
}
In [32]:
copy(ints, dupInts);
dupInts
Out[32]:
[1, 2, 3]
In [33]:
copy(ints, dupNums);
dupNums
Out[33]:
[1, 2, 3]

Due schemi riassuntivi

Una bella immagine di Andrey Tyukin può aiutare a riflettere sulle relazioni tra tipo e le nozioni di produttore e consumatore.

Per finire, una immagine tratta dal tutorial può aiutare a ricordare l'ordine indotto da queste relazioni