Home
Covarianza, controvarianza e invarianza in Java

22 Aprile 2019

Covarianza, controvarianza e invarianza in Java

di

Un estratto dal volume "Java 11" di Pellegrino Principe che spiega il tema della covarianza, controvarianza e invarianza in Java.

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> e Generic<Derived> è preservata rispetto a quella espressa da Base e Derived, ossia è lecito e type safe assegnare un’istanza di tipo Generic<Derived> in una variabile di tipo Generic<Base>. In modo più formale, data una classe generica C<T>, essa è definita covariante rispetto a T se la relazione di sottotipo tra le classi D <: B implica la stessa relazione di sottotipo tra le classi C<D> <: C<B>.
  • La relazione di sottotipo espressa da Generic<Base> e Generic<Derived> è invertita rispetto a quella espressa da Base e Derived, ossia è lecito e type safe assegnare un’istanza di tipo Generic<Base> in una variabile di tipo Generic<Derived>. In modo più formale, data una classe generica C<T>, essa è definita controvariante rispetto a T se la relazione di sottotipo tra le classi D <: B implica una relazione di sottotipo tra le classi C<B> <: C<D>.
  • La relazione di sottotipo espressa da Generic<Base> e Generic<Derived> è ignorata rispetto a quella espressa da Base e Derived, ossia non è lecito e type safe produrre degli assegnamenti tra un tipo Generic<Base> e un tipo Generic<Derived>. In modo più formale, data una classe generica C<T>, essa è definita invariante rispetto a T solo quando la relazione di sottotipo tra le classi C<D> <: C<B> è valida per D = 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).

Cosa accadrebbe senza controlli a compile time su tipi generici covarianti.

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 tipo Object.

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> e ICompare<T>;
  • una gerarchia di classi costituita dalla classe base Dog, da cui derivano la classe WhiteTerrier e la classe GoldenRetriever;
  • la classe DogComparer che implementa l’interfaccia ICompare<Dog>; saranno usate dal metodo main per dimostrare il funzionamento della controvarianza;
  • la classe MyList<T>, che deriva dalla classe java.util.ArrayList<T> e implementa l’interfaccia IEnumerate<T>; saranno usate dal metodo main 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 a tc (o gc) tutto funziona in modo type safe: a compile time il compilatore non genererà alcun messaggio d’errore, perché gli abbiamo assicurato che l’interfaccia ICompare è controvariante; a runtime la JVM non genererà alcuna eccezione, perché tc (o gc) faranno riferimento a un oggetto di tipo DogComparer, il cui metodo bornBefore avrà parametri di tipo Dog che potranno quindi accettare senza problemi gli argomenti di tipo WhiteTerrier e GoldenRetriever.

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
    }
}

Problema della covarianza degli array.

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 array wt di tipo WhiteTerrier, popolarlo con i suoi elementi e poi assegnarne il riferimento a una variabile di tipo Iterable<Dog> tramite un costrutto come il seguente: Iterable<Dog> dogs = Arrays.asList(wt).

L'autore

  • Pellegrino Principe
    Pellegrino Principe lavora come ricercatore informatico e capo sviluppatore software presso il Drappello Analisi e Studi della Sezione E-Government del Comando Generale della Guardia di Finanza di Roma dove è anche docente in Linguaggi di Programmazione. È appassionato dei linguaggi con una sintassi “C-like”.

Iscriviti alla newsletter

Novità, promozioni e approfondimenti per imparare sempre qualcosa di nuovo

Gli argomenti che mi interessano:
Iscrivendomi dichiaro di aver preso visione dell’Informativa fornita ai sensi dell'art. 13 e 14 del Regolamento Europeo EU 679/2016.

Libri che potrebbero interessarti

Tutti i libri

Fare la domanda giusta

L'arte di lavorare con ChatGPT e le AI

23,50

33,99€ -31%

19,00

20,00€ -5%

13,99

di Sergio Sentinelli, Alessandro Placa

CompTIA Security+

Guida aggiornata alla certificazione SY0-701

57,00

60,00€ -5%

di Mike Chapple, David Seidl


Articoli che potrebbero interessarti

Tutti gli articoli