Generics e Argomenti Wildcard Limitati in Java

Concetti Chiave
  • Anche agli argomenti generici wildcard possiamo applicare dei limiti, proprio come facciamo con i parametri di tipo.
  • Un argomento wildcard limitato è utile quando stiamo creando un tipo generico che opererà su una gerarchia di classi.
  • Un wildcard limitato può specificare sia un limite superiore che un limite inferiore.
  • Il limite superiore è specificato usando la parola chiave extends e rappresenta il super tipo massimo accettabile.
  • Il limite inferiore è specificato usando la parola chiave super e rappresenta il sotto tipo minimo accettabile.

Wildcard Limitati

Gli argomenti wildcard possono essere limitati nello stesso modo in cui un parametro di tipo può essere limitato.

Un argomento wildcard limitato è particolarmente importante quando stiamo creando un tipo generico che opererà su una gerarchia di classi. Per capire perché, lavoriamo attraverso un esempio.

Consideriamo la seguente gerarchia di classi che modellano delle coordinate cartesiane bidimensionali, tridimensionali e quadridimensionali:

// Coordinate bidimensionali.
class DueD {
    int x, y;

    DueD(int a, int b) {
        x = a;
        y = b;
    }

}

// Coordinate tridimensionali.
class TreD extends DueD {
    int z;

    TreD(int a, int b, int c) {
        super(a, b);
        z = c;
    }
}

// Coordinate quadridimensionali.
class QuattroD extends TreD {
    int t;

    QuattroD(int a, int b, int c, int d) {
        super(a, b, c);
        t = d;
    }
}

In cima alla gerarchia c'è DueD, che incapsula una coordinata bidimensionale XY. DueD è ereditata da TreD, che aggiunge una terza dimensione, creando una coordinata XYZ. TreD è ereditata da QuattroD, che aggiunge una quarta dimensione (tempo), producendo una coordinata quadridimensionale.

Mostrata di seguito è una classe generica chiamata Coordinate, che memorizza un array di coordinate:

// Questa classe contiene un array di oggetti coordinata.
class Coordinate<T extends DueD> {
    T[] coordinate;

    Coordinate(T[] o) {
        coordinate = o;
    }
}

Notiamo che Coordinate specifica un parametro di tipo limitato da DueD. Questo significa che qualsiasi array memorizzato in un oggetto Coordinate conterrà oggetti di tipo DueD o una delle sue sottoclassi.

Ora, assumiamo che vogliamo scrivere un metodo che visualizza le coordinate X e Y per ogni elemento nell'array coordinate di un oggetto Coordinate. Poiché tutti i tipi di oggetti Coordinate hanno almeno due coordinate (X e Y), questo è facile da fare usando un wildcard, come mostrato qui:

static void mostraXY(Coordinate<?> c) {
    System.out.println("Coordinate X Y:");

    for(int i=0; i < c.coordinate.length; i++)
        System.out.println(c.coordinate[i].x + " " +
                           c.coordinate[i].y);

    System.out.println();
}

Poiché Coordinate è un tipo generico limitato che specifica DueD come limite superiore, tutti gli oggetti che possono essere usati per creare un oggetto Coordinate saranno array di tipo DueD, o di classi derivate da DueD. Quindi, mostraXY() può visualizzare il contenuto di qualsiasi oggetto Coordinate.

Tuttavia, cosa succede se vogliamo creare un metodo che visualizza le coordinate X, Y e Z di un oggetto TreD o QuattroD?

Il problema è che non tutti gli oggetti Coordinate avranno tre coordinate, perché un oggetto Coordinate<DueD> avrà solo X e Y.

Quindi, come scriviamo un metodo che visualizza le coordinate X, Y e Z per oggetti Coordinate<TreD> e Coordinate<QuattroD>, impedendo che quel metodo venga usato con oggetti Coordinate<DueD>?

La risposta a questo problema è l'uso di un wildcard limitato.

Un wildcard limitato specifica sia un limite superiore sia un limite inferiore per l'argomento di tipo. Questo ci consente di restringere i tipi di oggetti sui quali un metodo opererà. Il wildcard limitato più comune è il limite superiore, che viene creato usando una clausola extends nello stesso modo in cui viene usata per creare un tipo limitato.

Usando un wildcard limitato, è facile creare un metodo che visualizza le coordinate X, Y e Z di un oggetto Coordinate, se quell'oggetto ha effettivamente quelle tre coordinate. Per esempio, il seguente metodo mostraXYZ() mostra le coordinate X, Y e Z degli elementi memorizzati in un oggetto Coordinate, se quegli elementi sono effettivamente di tipo TreD (o sono derivati da TreD):

static void mostraXYZ(Coordinate<? extends TreD> c) {
    System.out.println("Coordinate X Y Z:");

    for(int i=0; i < c.coordinate.length; i++)
        System.out.println(c.coordinate[i].x + " " +
                           c.coordinate[i].y + " " +
                           c.coordinate[i].z);

    System.out.println();
}

Notiamo che una clausola extends è stata aggiunta al wildcard nella dichiarazione del parametro c. Essa stabilisce che il tipo contrassegnato da ? può corrispondere a qualsiasi tipo finché è TreD, o una classe derivata da TreD. Quindi, la clausola extends stabilisce un limite superiore a cui il tipo ? può corrispondere.

A causa di questo limite, mostraXYZ() può essere chiamata con riferimenti a oggetti di tipo Coordinate<TreD> o Coordinate<QuattroD>, ma non con un riferimento di tipo Coordinate<DueD>. Tentare di chiamare mostraXZY() con un riferimento Coordinate<DueD> risulta in un errore di compilazione, garantendo così la sicurezza dei tipi.

Ecco un programma completo che dimostra le azioni di un argomento wildcard limitato:

// Argomenti Wildcard Limitati.

// Coordinate bidimensionali.
// Coordinate bidimensionali.
class DueD {
    int x, y;

    DueD(int a, int b) {
        x = a;
        y = b;
    }

}

// Coordinate tridimensionali.
class TreD extends DueD {
    int z;

    TreD(int a, int b, int c) {
        super(a, b);
        z = c;
    }
}

// Coordinate quadridimensionali.
class QuattroD extends TreD {
    int t;

    QuattroD(int a, int b, int c, int d) {
        super(a, b, c);
        t = d;
    }
}

// Questa classe contiene un array di oggetti coordinata.
class Coordinate<T extends DueD> {
    T[] coordinate;

    Coordinate(T[] o) {
        coordinate = o;
    }
}

// Dimostra un wildcard limitato.
class WildcardLimitato {

    static void mostraXY(Coordinate<?> c) {
        System.out.println("Coordinate X Y:");

        for(int i=0; i < c.coordinate.length; i++)
            System.out.println(c.coordinate[i].x + " " +
                               c.coordinate[i].y);

        System.out.println();
    }

    static void mostraXYZ(Coordinate<? extends TreD> c) {
        System.out.println("Coordinate X Y Z:");

        for(int i=0; i < c.coordinate.length; i++)
            System.out.println(c.coordinate[i].x + " " +
                               c.coordinate[i].y + " " +
                               c.coordinate[i].z);

        System.out.println();
    }

    static void mostraTutto(Coordinate<? extends QuattroD> c) {
        System.out.println("Coordinate X Y Z T:");
        for(int i=0; i < c.coordinate.length; i++)
            System.out.println(c.coordinate[i].x + " " +
                               c.coordinate[i].y + " " +
                               c.coordinate[i].z + " " +
                               c.coordinate[i].t);
        System.out.println();
    }

    public static void main(String[] args) {

        // Creiamo alcuni oggetti DueD.
        // Questi sono oggetti che hanno solo coordinate X e Y.
        DueD[] dueD = {
            new DueD(0, 0),
            new DueD(7, 9),
            new DueD(18, 4),
            new DueD(-1, -23)
        };

        Coordinate<DueD> posizioni2d =
            new Coordinate<DueD>(dueD);

        System.out.println("Contenuto di posizioni2d.");
        // OK, è un DueD
        mostraXY(posizioni2d);
        // Errore, non è un TreD
        // mostraXYZ(posizioni2d);
        // Errore, non è un QuattroD
        // mostraTutto(posizioni2d);

        // Ora, creiamo alcuni oggetti QuattroD.
        QuattroD[] quattroD = {
            new QuattroD(1, 2, 3, 4),
            new QuattroD(6, 8, 14, 8),
            new QuattroD(22, 9, 4, 9),
            new QuattroD(3, -2, -23, 17)
        };

        Coordinate<QuattroD> posizioni4d =
            new Coordinate<QuattroD>(quattroD);

        System.out.println("Contenuto di posizioni4d.");
        // Queste chiamate sono tutte valide
        mostraXY(posizioni4d);
        mostraXYZ(posizioni4d);
        mostraTutto(posizioni4d);
    }

}

L'output del programma è mostrato qui:

Contenuto di posizioni2d.
Coordinate X Y:
0 0
7 9
18 4
-1 -23

Contenuto di posizioni4d.
Coordinate X Y:
1 2
6 8
22 9
3 -2

Coordinate X Y Z:
1 2 3
6 8 14
22 9 4
3 -2 -23

Coordinate X Y Z T:
1 2 3 4
6 8 14 8
22 9 4 9
3 -2 -23 17

Notiamo queste righe commentate:

// Errore, non è un TreD
// mostraXYZ(posizioni2d);
// Errore, non è un QuattroD
// mostraTutto(posizioni2d);

Poiché posizioni2d è un oggetto Coordinate(DueD), non può essere usato per chiamare mostraXYZ() o mostraTutto() perché gli argomenti wildcard delimitati nelle loro dichiarazioni lo impediscono. Per provarlo, provate a rimuovere i simboli di commento, e poi tentate di compilare il programma. Riceverete errori di compilazione a causa dei disallineamenti di tipo.

In generale, per stabilire un limite superiore per un wildcard, usiamo il seguente tipo di espressione wildcard:

<? extends *superclasse*>

dove superclasse è il nome della classe che serve come limite superiore. Ricordiamo, questa è una clausola inclusiva perché la classe che forma il limite superiore (cioè, specificata da superclasse) è anch'essa entro i limiti.

Possiamo anche specificare un limite inferiore per un wildcard aggiungendo una clausola super a una dichiarazione wildcard. Ecco la sua forma generale:

<? super sottoclasse>

In questo caso, solo le classi che sono superclassi di sottoclasse sono argomenti accettabili. Anche questa è una clausola inclusiva.