Generics e Inferenza dei Tipi in Java

Concetti Chiave
  • L'inferenza dei tipi consente di semplificare la dichiarazione delle variabili generiche.
  • A partire da JDK 7, è possibile utilizzare l'operatore diamante per abbreviare la sintassi di creazione delle istanze di classi generiche.
  • L'inferenza del tipo di variabili locali, introdotta in JDK 10, può essere utilizzata con i generics per semplificare ulteriormente le dichiarazioni.

Inferenza dei Tipi con i Generics

A partire da JDK 7, è diventato possibile abbreviare la sintassi utilizzata per creare un'istanza di un tipo generico.

Per iniziare, consideriamo la seguente classe generica:

// Questa è una classe generica che accetta due argomenti di tipo.
class MiaClasse<T, V> {

    T oggetto1;
    V oggetto2;

    // Il costruttore accetta due argomenti di tipo T e V.
    MiaClasse(T o1, V o2) {
        oggetto1 = o1;
        oggetto2 = o2;
    }

/* ... */

}

Prima di JDK 7, per creare un'istanza di MiaClasse, avremmo dovuto utilizzare un'istruzione simile alla seguente:

MiaClasse<Integer, String> mioOggetto =
    new MiaClasse<Integer, String>(98, "Una Stringa");

Qui, gli argomenti di tipo (che sono Integer e String) sono specificati due volte: prima, quando mioOggetto viene dichiarato, e secondo, quando un'istanza di MiaClasse viene creata tramite new.

Da quando i generics sono stati introdotti da JDK 5, questa è la forma richiesta da tutte le versioni di Java precedenti a JDK 7. Sebbene non ci sia nulla di sbagliato, di per sé, questa forma è un po' più prolissa di quanto dovrebbe essere.

Nella clausola new, il tipo degli argomenti di tipo può essere facilmente inferito dal tipo di mioOggetto; pertanto, non c'è realmente alcun motivo per cui debbano essere specificati una seconda volta. Per affrontare questa situazione, JDK 7 ha aggiunto un elemento sintattico che consente di evitare la seconda specificazione.

Oggi la dichiarazione precedente può essere riscritta come mostrato qui:

MiaClasse<Integer, String> mioOggetto =
    new MiaClasse<>(98, "Una Stringa");

Si noti che la porzione di creazione dell'istanza utilizza semplicemente la sintassi <>, che è una lista di argomenti di tipo vuota.

Questo è chiamato operatore diamante (o diamond operator). Dice al compilatore di inferire gli argomenti di tipo necessari dal costruttore nell'espressione new. Il vantaggio principale di questa sintassi di inferenza del tipo è che abbrevia quelle che sono a volte dichiarazioni piuttosto lunghe.

Il precedente esempio può essere generalizzato. Quando viene utilizzata l'inferenza del tipo, la sintassi di dichiarazione per un riferimento generico e la creazione dell'istanza ha questa forma generale:

nome_classe<lista_arg_tipo> nome_var = new nome_classe<>(lista_arg_cons);

Qui, la lista degli argomenti di tipo del costruttore nella clausola new è vuota.

L'inferenza del tipo può anche essere applicata al passaggio di parametri. Ad esempio, se il seguente metodo viene aggiunto a MiaClasse,

boolean ugualeA(MiaClasse<T, V> o) {
    if(o.oggetto1 == o.oggetto1 && oggetto2 == o.oggetto2)
        return true;
    else
        return false;
}

allora la seguente invocazione è legale:

if(mioOggetto.ugualeA(new MiaClasse<>(1, "test")))
    System.out.println("Uguale");

In questo caso, gli argomenti di tipo per l'argomento passato a ugualeA() possono essere inferiti dal tipo del parametro.

Inferenza del Tipo di Variabili Locali e Generics

Come appena spiegato, l'inferenza del tipo è già supportata per i generics attraverso l'uso dell'operatore diamante.

Tuttavia, è anche possibile utilizzare la funzionalità di inferenza del tipo di variabile locale aggiunta da JDK 10 con una classe generica. Per esempio, assumendo MiaClasse utilizzata nella sezione precedente, questa dichiarazione:

MiaClasse<Integer, String> mioOggetto =
    new MiaClasse<Integer, String>(98, "Una Stringa");

può essere riscritta così utilizzando l'inferenza del tipo di variabile locale:

var mioOggetto =
    new MiaClasse<>(98, "Una Stringa");

In questo caso, il tipo di mioOggetto viene inferito e risulta essere MiaClasse<Integer, String> perché quello è il tipo del suo inizializzatore.

Si noti anche che l'uso di var risulta in una dichiarazione più breve di quanto sarebbe altrimenti. In generale, i nomi dei tipi generici possono spesso essere piuttosto lunghi e (in alcuni casi) complicati. L'uso di var è un altro modo per accorciare sostanzialmente tali dichiarazioni.