Salta al contenuto
Note per Studenti Esercizio - generics e riflessione con la coda e il caricatore di classi (esempi del corso)

Esercizio - generics e riflessione con la coda e il caricatore di classi (esempi del corso)

Esercizio basato sugli esempi del corso (Coda, CodaG, Main2, Main3, Loader2, Coccodrillo, Cane della cartella generics e reflect), sviluppati in un unico programma. Teoria: Java - generics, reflection e passaggio dei parametriIn Java ogni dato ha una propria identità a tempo di esecuzione (il cast è controllato e può lanciare ClassCastException, a differenza del C) e i parametri si passano sempre per valore (di un riferimento, per gli oggetti: si può modificare l'oggetto ma non riassegnare il riferimento del chiamante); i generics (CodaG) spostano i controlli di tipo a tempo di compilazione ma sono cancellati dalla type erasure, quindi non sono i template del C++; la riflessione (Class, Constructor, Method, Field) permette di caricare classi per nome, creare oggetti, chiamare metodi e leggere o scrivere campi a tempo di esecuzione, con eccezioni specifiche (ClassNotFoundException, InvocationTargetException, IllegalAccessException) e il costo di aggirare il controllo statico.Java - generics, reflection e passaggio dei parametri →. Il codice è stato compilato ed eseguito con JDK 21.

Testo

Gli esempi del corso contengono una coda circolare di Object (Coda), la sua versione generica CodaG<T> e i caricatori di classi che usano la riflessione.

Parte A (generics). Partendo da CodaG<T>:

  1. spiegare perché Main2 del corso (che inserisce nella Coda di Object le stringhe degli argomenti e poi una Coda) fallisce con ClassCastException e perché lo stesso errore, con CodaG<String>, non compila;
  2. rendere CodaG<T> iterabile (Iterable<T>);
  3. scrivere un metodo generico massimo che restituisca l'elemento maggiore di una CodaG<T> per qualunque T confrontabile;
  4. scrivere un metodo somma che accetti una coda di qualunque tipo numerico (CodaG<Integer>, CodaG<Double>...).

Parte B (riflessione). Scrivere una classe Caricatore con un metodo crea(String nomeClasse, String argomento), versione non interattiva del Loader2 del corso: carica la classe dal nome, stampa i costruttori, usa il costruttore con un solo parametro String se esiste (altrimenti quello senza parametri) e restituisce l'oggetto; un secondo metodo chiama(Object o, String metodo) invoca per nome un metodo senza parametri. Provarlo con Coccodrillo (tre costruttori, uno che lancia FileNotFoundException) e Cane (blocco static che stampa al caricamento). Infine mostrare che cosa arriva quando il costruttore lancia.

Soluzione

Parte A

1. Perché Main2 fallisce e Main3 no. Con Coda che contiene Object, add(argv[i]) e add(new Coda(0)) sono entrambi validi per il compilatore (tutto è un Object). Alla lettura il programma fa (String) c.take(): quando incontra la Coda, il cast controllato a runtime lancia ClassCastException, e l'eccezione compare dopo che le stringhe sono già state sommate. Con CodaG<String> il parametro di tipo fa sì che add(new Coda(0)) sia un errore di compilazione (il corso lo lascia commentato: //c.add(new Coda(0));, e analogamente ci.add("pippero") su CodaG<Integer>). Il controllo si sposta da runtime a compilazione, e non servono più i cast.

2. Iterable<T>. La classe implementa iterator() con una classe anonima che scorre da testa per size elementi, in modo circolare ((testa + i) % storage.length); così si può usare for (T x : coda).

3. massimo. Un metodo generico con vincolo: static <T extends Comparable<T>> T massimo(CodaG<T> c). Il vincolo permette di chiamare x.compareTo(m); senza di esso T sarebbe solo un Object. Funziona con Integer e con String (ordine alfabetico).

4. somma con wildcard. static double somma(CodaG<? extends Number> c) accetta qualunque coda di un sottotipo di Number; non si può scrivere CodaG<Number>, perché CodaG<Integer> non è un sottotipo di CodaG<Number> (i generics non sono covarianti). La wildcard vale in sola lettura: dalla coda si legge un Number, ma non vi si può aggiungere niente (non si sa di quale sottotipo sia). CodaG.somma(cs) con CodaG<String> non compila.

Parte B

Class.forName(nome) carica e inizializza la classe (Cane stampa subito la riga del blocco static); getDeclaredConstructors() fornisce i costruttori (l'ordine non è garantito dalla JVM: l'esecuzione sotto può elencarli in un ordine diverso); getDeclaredConstructor(String.class) cerca quello a un solo parametro String e, se manca, lancia NoSuchMethodException, che il metodo intercetta per ripiegare sul costruttore senza parametri (Cane). Il metodo chiamato per nome si cerca con getDeclaredMethod(nome) (nome e tipi dei parametri) e si esegue con invoke(oggetto).

Codice

java
import java.io.FileNotFoundException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.NoSuchElementException;

/** Parte A: la CodaG<T> del corso, resa Iterable, con metodi generici. */
class CodaG<T> implements Iterable<T> {
    private final T[] storage;
    private int testa, size;

    @SuppressWarnings("unchecked")
    CodaG(int len) { storage = (T[]) new Object[len]; }

    void add(T o) {
        if (size == storage.length) throw new IllegalStateException("Coda piena");
        storage[(testa + size) % storage.length] = o;
        size++;
    }

    T take() {
        if (size == 0) throw new NoSuchElementException("Coda vuota");
        T r = storage[testa];
        storage[testa] = null;
        testa = (testa + 1) % storage.length;
        size--;
        return r;
    }

    @Override public Iterator<T> iterator() {
        return new Iterator<T>() {
            int i = 0;
            public boolean hasNext() { return i < size; }
            public T next() { return storage[(testa + i++) % storage.length]; }
        };
    }

    /** Metodo generico con vincolo: T deve essere confrontabile con se' stesso. */
    static <T extends Comparable<T>> T massimo(CodaG<T> c) {
        T m = null;
        for (T x : c) if (m == null || x.compareTo(m) > 0) m = x;
        return m;
    }

    /** Wildcard: accetta CodaG<Integer>, CodaG<Double>... (si legge soltanto: "producer extends"). */
    static double somma(CodaG<? extends Number> c) {
        double s = 0;
        for (Number n : c) s += n.doubleValue();
        return s;
    }
}

/** Parte B: classi da caricare dinamicamente (come Coccodrillo e Cane del corso). */
class Coccodrillo {
    private final String msg;
    public Coccodrillo() throws FileNotFoundException { throw new FileNotFoundException("niente palude"); }
    public Coccodrillo(int i) { this.msg = "int " + i; }
    public Coccodrillo(String msg) { this.msg = msg; }
    public String saluta() { return "sono un coccodrillo <" + msg + ">"; }
}

class Cane {
    static { System.out.println("  [static] classe Cane caricata"); }
    public String saluta() { return "Bau"; }
}

/** Equivalente non interattivo del Loader2 del corso. */
class Caricatore {
    /** Istanzia con il costruttore a un solo parametro String, se esiste, altrimenti con quello di default. */
    static Object crea(String nomeClasse, String argomento) throws Exception {
        Class<?> clazz = Class.forName(nomeClasse);
        for (Constructor<?> k : clazz.getDeclaredConstructors()) System.out.println("  costruttore: " + k);
        try {
            Constructor<?> c = clazz.getDeclaredConstructor(String.class);
            return c.newInstance(argomento);
        } catch (NoSuchMethodException e) {
            return clazz.getDeclaredConstructor().newInstance();
        }
    }

    static Object chiama(Object o, String metodo) throws Exception {
        Method m = o.getClass().getDeclaredMethod(metodo);
        return m.invoke(o);
    }
}

public class GenericiRiflessione {
    public static void main(String[] args) throws Exception {
        CodaG<Integer> ci = new CodaG<>(5);
        ci.add(3); ci.add(9); ci.add(4);
        System.out.println("massimo = " + CodaG.massimo(ci) + ", somma = " + CodaG.somma(ci));
        CodaG<String> cs = new CodaG<>(5);
        cs.add("rosa"); cs.add("pendolo"); cs.add("baudolino");
        System.out.println("massimo (ordine alfabetico) = " + CodaG.massimo(cs));
        // CodaG.somma(cs);   // errore di compilazione: String non estende Number

        Object o1 = Caricatore.crea("Coccodrillo", "ciao");
        System.out.println(Caricatore.chiama(o1, "saluta"));
        Object o2 = Caricatore.crea("Cane", "ignorato");        // niente costruttore String: usa il default
        System.out.println(Caricatore.chiama(o2, "saluta"));

        // il costruttore di default di Coccodrillo lancia: arriva InvocationTargetException con la causa
        try {
            Coccodrillo.class.getDeclaredConstructor().newInstance();
        } catch (InvocationTargetException e) {
            System.out.println("causa: " + e.getCause());
        }
    }
}

Uscita:

massimo = 9, somma = 16.0
massimo (ordine alfabetico) = rosa
  costruttore: public Coccodrillo(java.lang.String)
  costruttore: public Coccodrillo(int)
  costruttore: public Coccodrillo() throws java.io.FileNotFoundException
sono un coccodrillo <ciao>
  [static] classe Cane caricata
  costruttore: Cane()
Bau
causa: java.io.FileNotFoundException: niente palude

Lettura dell'uscita:

  • massimo = 9, somma = 16.0 per la coda di interi 3, 9, 4; massimo (ordine alfabetico) = rosa per le stringhe;
  • il caricatore stampa i tre costruttori di Coccodrillo e usa quello String (sono un coccodrillo <ciao>); per Cane stampa [static] classe Cane caricata prima dell'elenco dei costruttori, ne trova uno solo (quello di default), e saluta() dà Bau;
  • il costruttore di default di Coccodrillo lancia FileNotFoundException: la riflessione la restituisce incapsulata in una InvocationTargetException, e la causa si legge con getCause() (causa: java.io.FileNotFoundException: niente palude). Una chiamata diretta new Coccodrillo() avrebbe invece obbligato a dichiarare o catturare l'eccezione controllata.

Versione ripasso

  • Main2 del corso: Coda di Object: add(argv[i]) e add(new Coda(0)) compilano entrambi; il cast (String) c.take() fallisce a runtime con ClassCastException. Con CodaG<String> add(new Coda(0)) non compila (il corso lo lascia commentato): controllo a compilazione, niente cast.
  • Iterabile: CodaG<T> implements Iterable<T>, iterator() scorre da testa per size in modo circolare.
  • Metodo generico con vincolo: static <T extends Comparable<T>> T massimo(CodaG<T> c); senza vincolo T sarebbe solo Object. Wildcard: static double somma(CodaG<? extends Number> c); CodaG<Integer> non è sottotipo di CodaG<Number>; la wildcard è in sola lettura.
java
static <T extends Comparable<T>> T massimo(CodaG<T> c) {
    T m = null;
    for (T x : c) if (m == null || x.compareTo(m) > 0) m = x;
    return m;
}
static double somma(CodaG<? extends Number> c) {
    double s = 0;
    for (Number n : c) s += n.doubleValue();
    return s;
}
  • Caricatore: Class.forName (carica e inizializza: Cane stampa il blocco static), getDeclaredConstructors() (ordine non garantito), getDeclaredConstructor(String.class) con ripiego su quello senza parametri se manca (NoSuchMethodException), getDeclaredMethod(nome).invoke(oggetto).
java
Class<?> clazz = Class.forName(nomeClasse);
try {
    return clazz.getDeclaredConstructor(String.class).newInstance(argomento);
} catch (NoSuchMethodException e) {
    return clazz.getDeclaredConstructor().newInstance();
}
  • Uscite: massimo = 9, somma = 16.0 (3, 9, 4); massimo (ordine alfabetico) = rosa; sono un coccodrillo <ciao>; [static] classe Cane caricata poi Bau; causa: java.io.FileNotFoundException: niente palude (costruttore che lancia →\to InvocationTargetException, causa in getCause()).
  • Perché Main3 non fallisce a runtime: CodaG<Integer> rifiuta ci.add("pippero") in compilazione; accumula += ci.take() non richiede né cast né Integer.parseInt; la somma degli argomenti si fa con unboxing automatico.
  • InvocationTargetException: il costruttore di default di Coccodrillo lancia FileNotFoundException; con newInstance l'eccezione arriva incapsulata e la causa si legge con getCause() (con new Coccodrillo() bisognerebbe dichiarare o catturare l'eccezione controllata).
  • Errori tipici: pensare che CodaG<Integer> sia un CodaG<Number>; dimenticare il vincolo Comparable e non poter chiamare compareTo; non gestire InvocationTargetException leggendone la causa; usare Class grezzo; contare sull'ordine di getDeclaredConstructors.

Teoria collegata