Generics e Errori di Ambiguità in Java

Concetti Chiave
  • Gli errori di ambiguità nei generics in Java si verificano quando due dichiarazioni generiche si risolvono nello stesso tipo cancellato, causando conflitti.
  • Per risolvere gli errori di ambiguità, è spesso necessario ristrutturare il codice o utilizzare nomi di metodi distinti.
  • La cancellazione dei generics può portare a situazioni in cui i metodi sovraccaricati diventano ambigui, specialmente quando i tipi generici sono simili o identici.

Errori di Ambiguità e Tipi Generici

L'inclusione dei generics in Java dà origine a un altro tipo di errore contro cui dobbiamo proteggerci: l'ambiguità.

Gli errori di ambiguità si verificano quando la cancellazione o erasure fa sì che due dichiarazioni generiche apparentemente distinte si risolvano nello stesso tipo cancellato, causando un conflitto.

Ecco un esempio che coinvolge l'overloading di metodi:

// Ambiguità causata dalla cancellazione su
// metodi sovraccaricati.
class MiaClasseGen<T, V> {
    // Dichiarazione di due oggetti di tipo T e V.
    T ogg1;
    V ogg2;

    /* ... */

    // Questi due metodi sovraccaricati sono ambigui
    // e non compileranno.
    void imposta(T o) {
        ogg1 = o;
    }

    // Questo metodo è ambiguo con il precedente
    void imposta(V o) {
        ogg2 = o;
    }
}

Notiamo che MiaClasseGen dichiara due tipi generici: T e V.

All'interno di MiaClasseGen, viene fatto un tentativo di sovraccaricare imposta() basato su parametri di tipo T e V. Questo sembra ragionevole perché T e V appaiono essere tipi diversi. Tuttavia, ci sono due problemi di ambiguità qui.

Primo, così come MiaClasseGen è scritta, non c'è alcun requisito che T e V siano effettivamente tipi diversi. Per esempio, è perfettamente corretto (in principio) costruire un oggetto MiaClasseGen come mostrato qui:

// In questo caso, sia T che V sono String.
MiaClasseGen<String, String> ogg =
    new MiaClasseGen<String, String>()

In questo caso, sia T che V saranno sostituiti da String. Questo rende entrambe le versioni di imposta() identiche, che è, ovviamente, un errore.

Il secondo e più fondamentale problema è che la cancellazione del tipo di imposta() riduce entrambe le versioni alla seguente:

void imposta(Object o) {
    // ...
}

Quindi, il sovraccarico di imposta() come tentato in MiaClasseGen è intrinsecamente ambiguo.

Gli errori di ambiguità possono essere difficili da correggere. Per esempio, se sappiamo che V sarà sempre qualche tipo di Number, potremmo provare a correggere MiaClasseGen riscrivendo la sua dichiarazione come mostrato qui:

// Quasi OK, ma ancora ambiguo.
class MiaClasseGen<T, V extends Number> {

Questa modifica fa sì che MiaClasseGen compili, e possiamo persino istanziare oggetti come quello mostrato qui:

MiaClasseGen<String, Number> x =
    new MiaClasseGen<String, Number>();

Questo funziona perché Java può determinare accuratamente quale metodo chiamare. Tuttavia, l'ambiguità ritorna quando proviamo questa riga:

MiaClasseGen<Number, Number> x =
    new MiaClasseGen<Number, Number>();

In questo caso, poiché sia T che V sono Number, quale versione di imposta() deve essere chiamata? La chiamata a imposta() è ora ambigua.

Francamente, nell'esempio precedente, sarebbe molto meglio usare due nomi di metodi separati, piuttosto che cercare di sovraccaricare imposta().

Spesso, la soluzione all'ambiguità coinvolge la ristrutturazione del codice, perché l'ambiguità spesso significa che abbiamo un errore concettuale nel nostro design.