Metodologie di Programmazione: Lezione 6

Riccardo Silvestri

Genericità

Spesso si vorrebbero definire classi o metodi che possano funzionare con valori di tipo variabile senza perdere il controllo statico sui tipi. Ad esempio in un metodo che cerca un valore in un array vorremmo imporre che il tipo del valore sia lo stesso (o un sotto-tipo) del tipo delle componenti dell'array. Però non vogliamo scrivere una versione per ogni possibile tipo, String, Integer, ecc. Vorremmo quindi che il tipo delle componenti dell'array sia trattato come una specie di parametro.

La genericità può essere sinteticamente descritta come uno strumento per trattare i tipi in modo parametrico. L'aggettivo generico è proprio usato per denotare un tipo o un metodo la cui definizione dipende da una o più variabili di tipo. In questo modo la definizione del tipo o metodo è generica perché può essere adattata semplicemente sostituendo le variabili di tipo con tipi specifici. L'istanziamento delle variabili di tipo, come vedremo, può essere esplicitamente specificata dal programmatore o è automaticamente inferita dal compilatore. Questo permette di trattare in modo uniforme tipi e metodi che altrimenti sarebbero dovuti essere specificatamente definiti in tantissime versioni tutte molto simili tra loro e al contempo mantiene il controllo statico sui tipi. A sua volta ciò migliora la leggibilità e il riuso, e facilita la manutenzione del codice.

Purtroppo la genericità in Java è più complessa di quella che sarebbe potuta essere perché è stata introdotta dopo che il linguaggio era già in uso da parecchi anni e c'era la necessità di mantenere la compatibilità con il codice pregresso. Questo rende impossibile esaurire l'argomento in una sola lezione. Per adesso tratteremo le principali caratteristiche rimandando a lezioni successive i necessari approfondimenti.

Metodi generici

Un metodo generico è un metodo in cui uno o più nomi di tipo sono sostituiti da parametri formali di tipo (formal type parameters) detti anche variabili di tipo (type variables). Quando il metodo è invocato le variabili di tipo sono sostituite con nomi di tipi specifici, secondo certe regole che vedremo fra poco. Le variabili di tipo devono essere dichiarate tra parentesi angolari, < >, nell'intestazione del metodo subito dopo gli eventuali modificatori e prima del tipo ritornato dal metodo.

Consideriamo un metodo find che preso in input un array e un valore (dello stesso tipo delle componenti dell'array) ritorna l'indice della prima posizione dell'array che contiene il valore o -1 se il valore non è presente. Volendo definire il metodo così che possa essere usato per array di qualsiasi tipo riferimento, lo definiamo generico rispetto al tipo delle componenti dell'array. Inoltre definiamo anche un main. Il tutto in una classe che chiamiamo Generic che ci servirà per fare esempi e test sulla genericità.

package mp;

import java.util.Objects;
import static java.lang.System.out;

/** Una classe per fare esempi e test sulla genericità */
public class Generic {
    /** Metodo generico che ritorna il primo indice dell'array in cui si trova
     * il valore, se non è presente ritorna -1.
     * @param a  un array
     * @param x  valore da cercare
     * @param <T>  variabile di tipo
     * @return  il primo indice in cui si trova il valore o -1 */
    public static <T> int find(T[] a, T x) {
        for (int i = 0 ; i < a.length ; i++)
            if (Objects.equals(a[i], x)) return i;
        return -1;
    }

    public static void main(String[] args) {
        String[] sA = new String[] {"A", "B", "C"};
        Integer[] intA = new Integer[] {12, 23, 1, 234};
        int k = find(sA, "C");             // T è sostituita con String
        out.println("k = "+k);
        k = find(intA, 2);                 // T è sostituita con Integer
        out.println("k = "+k);
        k = find(intA, "A");
        out.println("k = "+k);
    }
}

Il nome di una variabile di tipo può essere una qualsiasi sequenza di caratteri che rispetta le regole degli identificatori in Java (come i nomi delle classi). Per convenzione però il nome di una variabile di tipo è sempre una singola lettera maiuscola.

La variabile di tipo T è usata come se fosse il nome di un tipo effettivo. La differenza è che non ci sono tipi effettivi che si chiamano T e T è infatti dichiarata variabile di tipo tramite l'espressione <T>. In realtà non è sempre vero che una variabile di tipo può essere usata come se fosse il nome di un tipo effettivo, ci sono infatti alcune importanti eccezioni che vedremo fra poco. Quando il metodo generico find è invocato, il compilatore inferisce il tipo che deve essere "assegnato" alla variabile T. Una variabile di tipo può stare solamente per tipi riferimento, non può mai essere sostituita con tipi primitivi. Nella prima invocazione, siccome sia il tipo delle componenti dell'array del primo argomento che il tipo del secondo argomento è String, il tipo inferito è String. Nella seconda invocazione avviene una conversione boxing che converte il secondo argomento in un oggetto di tipo Integer che è anche il tipo delle componenti dell'array passato come primo argomento. Ne deriva che il tipo inferito è Integer. Nella terza invocazione i tipi relativi ai due argomenti sono differenti, il primo è Integer e l'altro è String. In questi casi il tipo inferito dal compilatore è il super-tipo comune più "vicino" ai tipi relativi agli argomenti (in questo caso il tipo inferito è Serializable & Comparable<?>1).

Se invece della variabile di tipo T avessimo usato direttamente il tipo Object avremmo potuto scrivere le stesse invocazioni del metodo. Ma allora, qual'è il vantaggio dell'uso della genericità? La versione generica del metodo permette di stabilire il tipo da "assegnare" alla variabile T al momento dell'invocazione. Il tipo da assegnare deve essere dichiarato tra parentesi angolari prima del nome del metodo. Però è necessario che l'invocazione del metodo sia qualificata appropriatamente tramite il nome della classe per metodi statici e con this o super per metodi non statici. Ecco alcuni esempi:

k = Generic.<String>find(intA, "A");    // ERRORE in compilazione
k = Generic.<String>find(sA, 3);        // ERRORE in compilazione
k = Generic.<Integer>find(intA, 2);     // OK

In questo modo è come se avessimo definito tantissime versioni (non generiche) del metodo find, una per il tipo String, una per il tipo Integer, e così via per ogni possibile tipo riferimento. Usato in questo modo il metodo generico permette di ottenere la massima generalità, come la versione che usa Object, mantenendo però il controllo statico sui tipi. Così l'incongruità di una invocazione come find(intA, "A"), che cerca una stringa in un array di interi, viene rilevata durante la compilazione. Si osservi che potrebbe essere molto difficile da rilevare perché non provoca lancio di eccezioni.

Quindi la genericità permette di scrivere un'unica versione generica di un metodo che sta per tutte le versioni che potrebbero essere scritte per tutti i tipi riferimento che possono essere sostituiti alle variabili di tipo. Inoltre il codice prodotto dal compilatore non è inutilmente gonfiato perché esiste un'unica versione compilata del metodo generico e questa è essenzialmente la stessa che sarebbe stata prodotta scrivendo il metodo con il tipo Object sostituito alle variabili di tipo (con l'aggiunta in certi casi di opportuni cast).

Definiamo ora un altro metodo (sempre della classe Generic) che preso in input un array e un valore assegna a tutte le componenti dell'array il valore dato.

/** Metodo generico che riempie l'array con il valore dato.
 * @param a  un array
 * @param x  valore di riempimento
 * @param <T>  variabile di tipo */
public static <T> void fill(T[] a, T x) {
    for (int i = 0 ; i < a.length ; i++)
        a[i] = x;
}

Consideriamo alcune invocazione nel main:

fill(intA, 13);                    // OK
fill(intA, "A");                   // ERRORE in esecuzione: ArrayStoreException
Generic.<Integer>fill(intA, "A");  // ERRORE in compilazione
fill(sA, "A");                     // OK
fill(sA, 12);                      // ERRORE in esecuzione: ArrayStoreException
Generic.<String>fill(sA, 12);      // ERRORE in compilazione
Generic.<String>fill(sA, "A");     // OK

Un'invocazione incongrua come fill(intA, "A") provoca un errore in esecuzione con lancio dell'eccezione ArrayStoreException perché si è tentato di assegnare un valore di tipo String a una componente di un array di Integer. Se si usa l'invocazione Generic.<Integer>fill(intA, "A") l'errore è rilevato in compilazione. A differenza di una versione non generica del metodo, con Object al posto di T, per cui non c'è nessun modo per far sì che lo stesso errore sia rilevabile in compilazione.

Il prossimo esempio mostra che la genericità permette di evitare (o perlomeno limitare) l'uso di cast che potrebbero fallire in esecuzione.

/** Metodo generico che ritorna l'elemento dell'array con la più lunga
 * stringa ritornata da {@code toString}.
 * @param a  un array
 * @param <T>  variabile di tipo
 * @return l'elemento dell'array con la più lunga {@code toString} */
public static <T> T longestStr(T[] a) {
    T val = null;
    int max = 0;
    for (T v : a)
        if (v != null && v.toString().length() >= max) {
            val = v;
            max = val.toString().length();
        }
    return val;
}

Ecco alcune invocazioni:

k = longestStr(intA);         // OK
String s = longestStr(sA);    // OK
s = longestStr(intA);         // ERRORE in compilazione: richiesto cast (String)

Nel metodo longestStr la variabile di tipo determina anche il tipo del valore ritornato. Quindi il tipo inferito dipende sia dal tipo dell'argomento che dal tipo della variabile a cui il valore ritornato dal metodo deve essere assegnato. Inoltre, se il tipo inferito non è compatibile con il tipo della variabile a cui il valore ritornato deve essere assegnato, si produce un errore in compilazione. Ad esempio l'invocazione s = longestStr(intA) provoca un errore in compilazione perché il tipo inferito è un super-tipo di Integer e quindi non può essere assegnato ad una variabile di tipo String. Si osservi inoltre che mentre l'invocazione lecita s = longestStr(sA) si può scrivere senza cast, la stessa invocazione per una versione non generica, con Object al posto di T, richiede un cast. Ma nel momento in cui si introduce un cast si corre il rischio che questo fallisca in esecuzione. Ad esempio, si potrebbe sostituire, inavvertitamente, sA con intA nell'invocazione e questo non sarebbe rilevato in compilazione mentre produrrebbe poi un errore in esecuzione.

Si osservi che nel metodo longestStr la variabile di tipo è usata anche per definire il tipo di una variabile locale. Come abbiamo già detto una variabile di tipo può essere usata come se fosse il nome di un tipo effettivo. Però c'è un'importante eccezione: le variabili di tipo non possono essere usate in espressioni creazionali come new T(...) o new T[...]. Inoltre, non si possono usare con l'operatore instanceof. Vedremo in seguito come si possono creare array di tipo parametrico. La ragione che impedisce di creare direttamente array di tipo parametrico o oggetti il cui tipo è una variabile di tipo è che Java cancella al runtime tutti i parametri di tipo sostituendoli con Object. Questa si chiama tecnicamente type erasure e i tipi dopo la cancellazione sono detti raw types. Al runtime la JVM non ha più alcuna informazione circa i parametri di tipo. Quindi qualsiasi cosa che richiede la conoscenza del valore di una variabile di tipo al runtime non è permessa.

Tipi generici

Al pari dei metodi generici che aggiungono flessibilità al linguaggio senza sacrificare il controllo statico sui tipi, Java supporta anche la definizione di tipi generici (generic types). Un tipo generico è una classe o una interfaccia che nella sua dichiarazione ha una o più variabili di tipo (dichiarate tra parentesi angolari). Una variabile di tipo è usata nella definizione della classe/interfaccia alla stregua di un qualsiasi nome di tipo effettivo, con le stesse limitazioni viste per i metodi generici. Ogni tipo generico definisce un insieme di tipi parametrici (parameterized types) che consistono nel nome della classe o interfaccia seguito da una lista di nomi di tipi effettivi (tra parentesi angolari) corrispondenti alle variabili di tipo.

Classi generiche

Consideriamo un semplice esempio di classe generica che rappresenta una coppia di valori e la definiamo in Generic.java:

/** Un oggetto {@code Pair} rappresenta una coppia di valori dello stesso tipo
 * @param <T>  tipo dei valori della coppia */
class Pair<T> {
    /** Crea una coppia di valori.
     * @param first  primo valore della coppia
     * @param second  secondo valore della coppia */
    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() { return first; }
    public T getSecond() { return second; }

    public void setFirst(T x) { first = x; }
    public void setSecond(T x) { second = x; }

    private T first, second;
}

La variabile di tipo T è dichiarata, come per i metodi generici, tra parentesi angolari e immediatamente dopo il nome della classe (o dell'interfaccia). Poi, nella definizione della classe può essere usata come se fosse il nome di un tipo effettivo (eccetto che nelle espressioni creazionali). Per istanziare un oggetto tramite un tipo generico si può specificare esplicitamente quali sono i tipi che devono essere sostituiti alle corrispondenti variabili di tipo oppure si può lasciare che sia il compilatore ad inferirli e in quest'ultimo caso basta scrivere <> (diamond) subito dopo il nome del tipo. Ecco alcuni esempi di uso di questo tipo generico:

Pair<String> sp = new Pair<>("primo", "secondo");
sp.setFirst("I");        // OK
sp.setSecond(2);         // ERRORE in compilazione

Pair<Integer> ip = new Pair<>(13, 17);    // Boxing
ip.setFirst(11);                // Boxing
ip.setSecond("13");             // ERRORE in compilazione
int k = ip.getFirst();          // Unboxing
String s = ip.getFirst();       // ERRORE in compilazione

Pair<Pair<String>> spp = new Pair<>(sp, sp);   // OK

Per istanziare una coppia di stringhe si usa il tipo parametrico Pair<String> e per istanziare una coppia di interi si usa Pair<Integer>. L'ultima riga mostra che un tipo parametrico può a sua volta essere usato per creare un altro tipo parametrico (una coppia di coppie di stringhe). Una volta istanziato un tipo parametrico diventa del tutto simile a un tipo non generico definito direttamente con i tipi effettivi al posto delle variabili di tipo. Però il tipo (non generico) corrispondente ad un tipo parametrico non viene esplicitamente definito dal compilatore. Come per i metodi generici, il codice compilato contiene una sola classe che corrisponde al tipo generico e che è usata per tutti i tipi parametrici. Tale classe è molto simile a quella che si ottiene sostituendo la variabile di tipo con Object, cioè è il raw type ottenuto tramite la type erasure che abbiamo già discusso. Questo ha importanti conseguenze alcune delle quali saranno discusse ora e altre più avanti. Consideriamo i seguenti esempi

Dipendente d1 = new Dipendente("Mario Rossi"), d2 = new Dipendente("Ugo Gialli");
Pair<Dipendente> dp = new Pair<>(d1, d2);
dp.setFirst(new Dirigente("Carla Bo", 100));     // OK

Dirigente dir1 = new Dirigente("Ciro Blu", 200);
Dirigente dir2 = new Dirigente("Ada Verdi", 300);
Pair<Dirigente> dirp = new Pair<>(dir1, dir2);
dirp.setFirst(d1);           // ERRORE in compilazione
dp = dirp;                             // 1. ERRORE in compilazione
dp.setFirst(d1);                       // 2. se (1) non fosse un errore allora...
double b = dirp.getFirst().getBonus(); // 3. qui si avrebbe un errore in esecuzione

Pair<Object> objp = new Pair<>("B", 13);    // OK
objp.setFirst(dir1);                        // OK
objp = dp;                                  // ERRORE in compilazione

L'assegnamento dp = dirp provoca un errore in compilazione perché il tipo di dirp, che è Pair<Dirigente>, non è un sotto-tipo del tipo di dp, che è Pair<Dipendente>. Questo può apparire sorprendente ma se così non fosse allora sarebbero possibili le istruzioni (2) e (3). L'istruzione (3) provocherebbe un errore in esecuzione perché il primo elemento della coppia in dirp è stato posto dall'istruzione (2) uguale ad un oggetto di tipo Dipendente il quale non ha il metodo getBonus. Quindi se Pair<Dirigente> fosse trattato come un sottotipo di Pair<Dipendente> la genericità non potrebbe garantire la sua proprietà fondamentale: il controllo statico sui tipi. D'altronde se immaginiamo delle classi DipendentePair e DirigentePair che definiscono direttamente coppie di Dipendente e coppie di Dirigente non c'è ragione perché debbano essere l'una il sotto-tipo dell'altra. In generale, quindi, se Type1 e Type2 sono due qualsiasi tipi distinti allora Pair<Type1> e Pair<Type2> non hanno nessuna relazione di sotto-tipo o super-tipo fra loro. Questo è in contrasto con il comportamento covariante degli array: se Type1 è un sotto-tipo di Type2 allora Type1[] è un sotto-tipo di Type2[] (il tipo degli array varia come il tipo delle componenti). Mentre i tipi generici sono invarianti. Infatti un array, a differenza dei tipi parametrici, mantiene nel codice compilato il tipo delle componenti. E deve essere così perché altrimenti il seguente errore

Dirigente[] dirA = new Dirigente[3];
Dipendente[] dA = dirA;   // OK, perché Dirigente[] è sotto-tipo di Dipendente[]
dA[0] = new Dipendente("Ugo Blu", 100);             // ERRORE solo in esecuzione

non sarebbe rilevabile, neanche in esecuzione. Questa è la ragione per cui non è possibile creare (almeno direttamente) array le cui componenti sono di un tipo parametrico:

Pair<String>[] spA;                // OK
spA = new Pair<String>[3];         // ERRORE in compilazione

Però, come si vede, è possibile dichiarare variabili il cui tipo è un array di tipo parametrico. La ragione di questa stranezza è che in realtà è possibile creare indirettamente array di tipo parametrico. Uno dei modi (ma non è l'unico) è il seguente:

Pair<String>[] arrSP(Pair<String>...p) {
    Pair<String>[] aP = p;   // Array di tipo parametrico
    return aP;               // Ritorna un array di tipo parametrico
}

La classe generica Pair ha una sola variabile di tipo. Diamo ora un esempio che usa due variabili di tipo. Si tratta di una versione più generale della classe Pair<T> perché permette che i tipi delle due componenti possano essere diversi.

/** Un oggetto {@code DPair} rappresenta una coppia di valori di tipi anche diversi.
 * @param <F>  tipo del primo valore della coppia
 * @param <S>  tipo del secondo valore della coppia */
class DPair<F, S> {
    /** Crea una coppia di valori.
     * @param first  primo valore della coppia
     * @param second  secondo valore della coppia */
    public DPair(F first, S second) {
        this.first = first;
        this.second = second;
    }

    public F getFirst() { return first; }
    public S getSecond() { return second; }

    public void setFirst(F x) { first = x; }
    public void setSecond(S x) { second = x; }

    private F first;
    private S second;
}

Avremmo potuto definire prima questa classe generica e poi Pair<T> come sotto-classe (generica):

class Pair<T> extends DPair<T, T> {
    public Pair(T first, T second) {
        super(first, second);
    }
}

Se avessimo fatto così, per ogni tipo specifico Type, si avrebbe che Pair<Type> è un sotto-tipo di DPair<Type, Type>. D'altronde ciò è in accordo con l'intuizione che equipara Pair<Type> ad una classe non generica che è definita estendendo una classe (non generica) che corrisponde a DPair<Type, Type>. Ovviamente questo vale in generale, non solo per Pair e DPair.

Interfacce generiche

Non solo le classi ma anche le interfacce possono essere generiche. È anzi più facile incontrare esempi di interfacce generiche che di classi generiche e di solito le classi generiche sono implementazioni di interfacce generiche. Per le interfacce valgono le stesse regole di dichiarazione e uso delle variabili di tipo. Iniziamo considerando una delle interfacce generiche più implementate e usate della libreria di Java. L'interfaccia Comparable<T>, del package java.lang, serve ad imporre un ordinamento totale degli oggetti della classe che la implementa. Riportiamo la definizione:

public interface Comparable<T> {
    int compareTo(T obj);
}

Il metodo compareTo(T obj) deve ritornare un intero negativo se l'oggetto su cui è invocato è minore di obj, un intero positivo se invece è maggiore di obj e zero se sono uguali. Più di 100 classi della libreria Java implementano questa interfaccia. Ad esempio, le otto classi che corrispondono ai tipi primitivi, String, Date, ecc. Ovviamente, le classi implementano un opportuno tipo parametrico (o interfaccia parametrica) che deriva dall'interfaccia generica Comparable<T>. La classe String implementa Comparable<String>, la classe Integer implementa Comparable<Integer>, Double implementa Comparable<Double> e così via.

Modifichiamo la definizione della classe Dipendente implementando l'interfaccia Comparable<Dipendente> che confronta i dipendenti in base al loro codice:

public class Dipendente implements Comparable<Dipendente> {
     . . .
    @Override
    public int compareTo(Dipendente d) {
        long c = d.getCodice();
        return (c < codice ? -1 : (c > codice ? 1 : 0));
    }
    . . .
}

L'interfaccia generica Comparable<T> permette di implementare metodi generici che si basano su una relazione di ordinamento. Ad esempio, trovare il minimo di un array o un insieme, trovare il massimo, ordinare un array, cercare un valore in un array tramite ricerca binaria, ecc. Proviamo allora a definire un metodo generico che ritorna il valore minimo di un array:

public static <T> T min(T[] a) {
    T min = a[0];
    for (T v : a)
        if (v.compareTo(min) < 0) // ERRORE in compilazione: non trova compareTo
            min = v;
    return min;
}

Questa implementazione non va bene perché il metodo compareTo non è un metodo come quelli di Object che appartengono a tutti i tipi riferimento e quindi a tutti i possibili tipi che la variabile T può rappresentare. Affinché si possa scrivere un metodo generico come questo occorre che la variabile di tipo sia limitata ai tipi T che implementano l'interfaccia Comparable<T>. Ciò è possibile:

public static <T extends Comparable<T>> T min(T[] a) {
    T min = a[0];
    for (T v : a)
        if (v.compareTo(min) < 0)
            min = v;
    return min;
}

La dichiarazione <T extends Comparable<T>> significa proprio che la variabile di tipo T varia solamente tra i tipi che sono sotto-tipi di Comparable<T>, cioè, i tipi che implementano tale interfaccia. In altre parole, un tipo effettivo Type può sostituire la variabile di tipo T se e solo se Type implementa l'interfaccia Comparable<Type>. Per chiarire bene questo punto consideriamo un frammento di codice che usa il metodo min:

Dipendente[] dd = new Dipendente[] {new Dipendente("Ugo Blu", 0, 12),
        new Dipendente("Ada Palla", 0, 7), new Dipendente("Lia Gro", 0, 9)};
Dipendente minD = min(dd);              // OK
Integer[] iA = new Integer[] {3, 4, 1, 0, -3, 6};
int m = min(iA);                        // OK
Dirigente[] dirA = new Dirigente[] {new Dirigente("Carla Bo", 100),
        new Dirigente("Luisa Lalla", 200)};
Dirigente minDir = min(dirA);           // ERRORE in compilazione

L'ultima riga provoca un errore in compilazione perché la classe Dirigente non implementa l'interfaccia Comparable<Dirigente>. Però la classe Dirigente, estendendo Dipendente, implementa l'interfaccia Comparable<Dipendente>. E potrebbe essere che l'ordinamento inteso per gli oggetti di tipo Dirigente sia proprio quello ereditato dalla super-classe Dipendente. Per casi come questo si vorrebbe poter scrivere un metodo generico che è usabile anche quando l'interfaccia Comparable è implementata da una super-classe o più in generale, quando la classe implementa l'interfaccia Comparable relativamente a un super-tipo. Vedremo che questo è invero possibile. Troveremo molti altri esempi di interfacce e classi generiche quando tratteremo le collezioni della libreria Java.

Esercizi

[ErroriMG1]    Il seguente programma contiene uno o più errori. Trovare gli errori e spiegarli. In particolare, dire per ogni errore se si verifica in compilazione o durante l'esecuzione.

import java.util.Objects;

public class Test {
    public static <T> T first(T[] a, T[] b) {
        int i = 0;
        while (i < a.length && i < b.length && !Objects.equals(a[i], b[i]))
            i++;
        return (i < a.length && i < b.length ? a[i] : null);
    }
    public static void main(String[] args) {
        long[] longA = {2, new Long(5)};
        int[] intA = {1, 2, 3};
        int val = first(longA, longA);
        val = first(longA, intA);
        Integer[] intA2 = {2, 3, 4};
        Long[] longA2 = {1L, 2L, 3L};
        val = first(intA2, longA2);
        Long vL = first(intA2, longA2);
        Number num = first(intA2, longA2);
    }
}

[ErroriMG2]    Il seguente programma contiene uno o più errori. Trovare gli errori e spiegarli. In particolare, dire per ogni errore se si verifica in compilazione o durante l'esecuzione.

public class Test {
    public static <E> E sub(E[] a, E v) {
        int i = 0;
        while (i < a.length && !v.toString().equals(a[i].toString()))
            i++;
        if (i < a.length) {
            E c = a[i];
            a[i] = v;
            return c;
        } else return null;
    }
    public static void main(String[] args) {
        String[] sA = {"A", "B", "C"};
        String s = sub(sA, "D");
        Integer[] intA = {1, 2, 3};
        int k = sub(intA, 13);
        k = sub(intA, "2"); 
        Object obj = sub(intA, "3");
        obj = Test.<Integer>sub(intA, '4');
        Object[] objA = {2, 3, 4};
        Integer v = sub(objA, 2);
    }
}

[ElementiComuni]    Scrivere un metodo generico che presi in input due array ritorna il numero di elementi del primo array che sono presenti anche nel secondo array. Scrivere anche una versione non generica usando il tipo Object. Confrontare le due versioni e discutere i vantaggi e i svantaggi dei loro possibili usi.

[ValoreComune]    Scrivere un metodo generico che presi in input due array ritorna il primo valore del primo array che appare anche nel secondo. Se non ci sono valori in comune ritorna null. Scrivere anche una versione non generica del metodo e discutere i vantaggi e i svantaggi dei possibili usi delle due versioni.

[StampaMatrici]    Scrivere un metodo generico che presa in input una matrice la stampa in modo che le colonne siano allineate come nei seguenti esempi:

MATRICE di Double                      MATRICE di String
0.123    23.5    12.01                 Roma    Milano           Genova
1        0.4     1.234                 Napoli  Reggio Calabria  Teramo
1234567  67.897  12                    Terni   Palermo          Perugia

Scrivere anche una versione non generica e discutere i vantaggi e i svantaggi dei possibili usi delle due versioni.

[ErroriG1]    Il seguente programma contiene uno o più errori. Trovare gli errori e spiegarli. In particolare, dire per ogni errore se si verifica in compilazione o durante l'esecuzione (si assume che le classi siano definite nello stesso package di DPair).

class LDPair<F, S> extends DPair<F, S> {
    public LDPair(F f, S s, String l) {
        super(f, s);
        label = l;
    }
    public String getLabel() { return label; }
    private String label;
}
public class Test {
    public static void main(String[] args) {
        DPair<Integer, Number> dp = new LDPair<Integer, Number>(12, 2.6, "A");
        String s = dp.getLabel();
        DPair<Object, Object> objp = new DPair<String, String>("A", "B");
        DPair<Long, Long>[] pA = new DPair<>[5];
        DPair<DPair<Long, Long>, int[]> t = new DPair<>(new DPair<Long, Long>(1L, 1L), 
                                                        new int[5]);
    }
}

[ErroriG2]    Il seguente programma contiene uno o più errori. Trovare gli errori e spiegarli. In particolare, dire per ogni errore se si verifica in compilazione o durante l'esecuzione.

class CN implements Comparable<Integer> {
    public CN(String v) { val = v; }

    public int compareTo(Integer o) {
        if (o == null) throw new NullPointerException();
        int len = val.length();
        return (len < o ? -1 : (len > o ? 1 : 0));
    }
    private String val;
}

public class Test {
    public static <T extends Comparable<Integer>> boolean below(T[] a, int b) {
        for (T v : a)
            if (v.compareTo(b) > 0) return false;
        return true;
    }
    public static void main(String[] args) {
        CN[] a = new CN[] {new CN("A"), new CN("B")};
        boolean r = below(a, 13);
        Integer[] intA = new Integer[] {1, 2, 3};
        r = below(intA, 10);
        int[] iA = {1, 2, 3};
        r = below(iA, 5);
    }
}

[PuntiGenerici]    Definire una versione generica GLPoint<T> della classe LPoint in cui la label ha tipo generico T. La classe GLPoint<T> deve estendere la classe Point (punto senza label). Così LPoint corrisponderebbe a GLPoint<String>.

[Copia&Scambia]    Definire un metodo generico copySwap che presa in input una coppia di tipo Pair<T>, crea e ritorna una nuova coppia dello stesso tipo con i valori della coppia di input scambiati.

[Minmax]    Scrivere un metodo generico che preso in input un array ritorna il valore minimo e il valore massimo dell'array. Usare Pair<T> per ritornare la coppia di valori.

[ContaValore]    Scrivere un metodo generico che preso in input un array ritorna il valore più frequente e il numero di occorrenze (usare in modo opportuno il tipo DPair<F, S> per ritornare la coppia valore e numero occorrenze). Ecco alcuni esempi di input e output del metodo:

INPUT                                        OUTPUT
{"B", "AB", "A", "B"}                        ("B", 2)
{2, 13, 2, 1, 13, 13}                        (13, 3) 

[InsiemiGenerici]    Definire una classe GSet<T> per rappresentare insiemi i cui elementi sono di tipo (generico) T. Implementare dei metodi per le seguenti operazioni:

[Multinsieme]    Definire una classe generica MSet<T> per rappresentare multi-insiemi i cui elementi sono di tipo (generico) T. Un multi-insieme a differenza di un insieme può contenere uno stesso elemento più volte. In altri termini ogni elemento appartenente al multi-insieme vi appartiene con una certa molteplicità (1, 2, 3, ...). Implementare dei metodi per le seguenti operazioni:

Suggerimento: rappresentare ogni elemento con una coppia (valore, molteplicità).

[ContaValori]    Scrivere un metodo generico che preso in input un array ritorna un MSet<T> (vedi l'esercizio precedente) che rappresenta i valori presenti nell'array. Ecco un esempio:

ARRAY           {"A", "AB", "B", "A", "AB", "AB"}
MULTI-INSIEME   {("A", 2), ("AB", 3), ("B", 1)}

8 Mar 2016


  1. Si è legittimati ad esclamare “mamma mia!” o se si è madrelingua inglese, “oh my God!”.