Salta al contenuto
Note per Studenti Java - generics, reflection e passaggio dei parametri

Java - generics, reflection e passaggio dei parametri

In questa pagina 5

Questa nota raccoglie le lezioni "Un po' di Java" del corso: tre argomenti che servono per tutto il resto (pattern, test, concorrenza). Chi viene dal C trova le differenze di fondo in Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C → e Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →; chi viene dal C++ confronti con C++ - funzioni, reference e passaggio dei parametriuna funzione ha parametri formali (0 o più, input) e un valore di ritorno (0 o 1, output); in C++ gli argomenti si passano per valore, cioè il parametro è una variabile locale inizializzata con una copia dell'argomento, e modificarla non cambia l'originale; per oggetti grandi (vector, immagini) la copia costa, e si passa per reference (T&), un alias dell'oggetto del chiamante che permette lettura e scrittura, oppure per const reference (const T&), alias in sola lettura verificata dal compilatore, che accetta anche literal (oggetto temporaneo); regola: valore per oggetti piccoli, const T& per grandi da non modificare, T& solo se la funzione deve davvero modificare o produrre più di un output; il passaggio converte l'argomento nel tipo del parametro come in un'inizializzazione e un cast esplicito (static_cast) rende visibile l'intenzione.C++ - funzioni, reference e passaggio dei parametri → e C++ - template e programmazione genericaun template permette di usare un tipo (o un intero noto in compilazione) come parametro di una classe o di una funzione; il compilatore genera il codice per ogni tipo per cui il template è specializzato (template instantiation, a tempo di compilazione): il codice è efficiente ma gli errori sono lunghi e oscuri, e il compilatore controlla sia il template sia il suo argomento; la notazione è template (o class T, equivalenti) davanti alla classe o alla funzione, e le funzioni membro fuori dalla classe si scrivono template tipo Classe::nome(...); nelle function template il compilatore deduce gli argomenti dai parametri della chiamata (myMax(i, j) invece di myMax(i, j)), ma due argomenti di tipo diverso per lo stesso T sono un errore; una specializzazione esplicita (template<> ...) fornisce il codice per un tipo particolare; i parametri interi (template<typename T, int N> struct array) danno array di dimensione fissa senza free store, più efficienti e che esprimono concetti come la tripletta RGB; poiché il compilatore deve vedere la definizione completa al punto d'uso, le definizioni si mettono nell'header (spesso in un file .hpp incluso in coda al .h).C++ - template e programmazione generica →. Tutti i programmi sono stati compilati ed eseguiti con JDK 21.

Java e C: identità dei dati e cast

Definizione (identità dei dati). In Java ogni oggetto conosce la propria classe a tempo di esecuzione. In C un cast cambia solo l'interpretazione dei byte da parte del compilatore.

  • Il cast (Integer) o di Java viene controllato a tempo di esecuzione: se o non è un Integer si ha ClassCastException. Il cast di C non controlla niente.
  • Un array di byte resta un array di byte (in un driver di rete i 4 byte di un indirizzo IPv4 non diventano un int per magia): meno efficiente a livello di hardware, ma sicuro.
  • Linking e caricamento. Il C è dynamically linked (le librerie sono collegate prima dell'esecuzione; con -static si includono nell'eseguibile, che cresce molto di dimensione). Java usa il lazy binding: una classe viene cercata e caricata solo al primo uso, "all'ultimo momento". È la base della riflessione (vedi sotto).
  • La documentazione del JDK (docs.oracle.com/en/java/javase/21/docs/api) va consultata da subito: i nomi di classi e metodi si cercano lì.

Passaggio dei parametri

Definizione. In Java il passaggio dei parametri è sempre per valore. Per i tipi primitivi si copia il valore; per gli oggetti si copia il riferimento (l'indirizzo dell'oggetto), quindi il chiamante e il metodo vedono lo stesso oggetto ma hanno due variabili diverse che lo riferiscono.

Conseguenze:

che cosa fa il metodo effetto fuori dal metodo
modifica un parametro primitivo nessuno (è una copia)
riassegna il parametro oggetto (s = new ...) nessuno (cambia solo la copia locale del riferimento)
modifica lo stato dell'oggetto (s.copie = 99) visibile al chiamante
modifica il contenuto di un array visibile (gli array sono oggetti)
"modifica" una String nessuno: le stringhe sono immutabili, s + "!" crea un nuovo oggetto
scambia due parametri oggetto nessuno: non si può scrivere swap(a, b) in Java

Stato in memoria di riassegna(s): prima della chiamata s e il parametro s del metodo puntano entrambi a Scheda(1) sull'heap; dopo s = new Scheda(99) il parametro punta a un nuovo oggetto, la variabile del chiamante no.

passo variabile s del chiamante (stack) parametro s del metodo (stack) heap
ingresso nel metodo → A → A A = Scheda(1)
dopo s = new Scheda(99) → A → B A = Scheda(1), B = Scheda(99)
uscita dal metodo → A (scompare) A = Scheda(1); B senza riferimenti
java
import java.util.Arrays;

class Scheda {
    int copie;
    Scheda(int copie) { this.copie = copie; }
    @Override public String toString() { return "Scheda(" + copie + ")"; }
}

public class PassaggioParametri {
    static void incrementaPrimitivo(int x) { x = x + 1; }

    static void riassegna(Scheda s) { s = new Scheda(99); }          // cambia la copia del riferimento

    static void modifica(Scheda s) { s.copie = 99; }                  // cambia l'oggetto condiviso

    static void scambia(Scheda a, Scheda b) { Scheda t = a; a = b; b = t; }   // non scambia nulla fuori

    static void riempi(int[] v) { Arrays.fill(v, 7); }

    static String allunga(String s) { s = s + "!"; return s; }        // le stringhe sono immutabili

    public static void main(String[] args) {
        int n = 5;
        incrementaPrimitivo(n);
        System.out.println("n dopo incrementaPrimitivo = " + n);

        Scheda s = new Scheda(1);
        riassegna(s);
        System.out.println("dopo riassegna: " + s);
        modifica(s);
        System.out.println("dopo modifica:  " + s);

        Scheda a = new Scheda(1), b = new Scheda(2);
        scambia(a, b);
        System.out.println("dopo scambia: a = " + a + ", b = " + b);

        int[] v = new int[3];
        riempi(v);
        System.out.println("array dopo riempi: " + Arrays.toString(v));

        String t = "ciao";
        allunga(t);
        System.out.println("t dopo allunga (valore scartato) = " + t);
        t = allunga(t);
        System.out.println("t dopo t = allunga(t) = " + t);
    }
}

Uscita:

n dopo incrementaPrimitivo = 5
dopo riassegna: Scheda(1)
dopo modifica:  Scheda(99)
dopo scambia: a = Scheda(1), b = Scheda(2)
array dopo riempi: [7, 7, 7]
t dopo allunga (valore scartato) = ciao
t dopo t = allunga(t) = ciao!

A differenza del C++ (C++ - funzioni, reference e passaggio dei parametriuna funzione ha parametri formali (0 o più, input) e un valore di ritorno (0 o 1, output); in C++ gli argomenti si passano per valore, cioè il parametro è una variabile locale inizializzata con una copia dell'argomento, e modificarla non cambia l'originale; per oggetti grandi (vector, immagini) la copia costa, e si passa per reference (T&), un alias dell'oggetto del chiamante che permette lettura e scrittura, oppure per const reference (const T&), alias in sola lettura verificata dal compilatore, che accetta anche literal (oggetto temporaneo); regola: valore per oggetti piccoli, const T& per grandi da non modificare, T& solo se la funzione deve davvero modificare o produrre più di un output; il passaggio converte l'argomento nel tipo del parametro come in un'inizializzazione e un cast esplicito (static_cast) rende visibile l'intenzione.C++ - funzioni, reference e passaggio dei parametri →) non esistono parametri per riferimento (int&): per restituire due valori si restituisce un oggetto o un array.

Esempio dal corso (EsempioPassaggioParametri). Il metodo int func1(String a) dichiara int zz; e poi fa if(zz < 0). Il compilatore rifiuta il programma: error: variable zz might not have been initialized. Le variabili locali non hanno valore iniziale (il compilatore impone l'assegnamento prima dell'uso); invece i campi (come String s; nella stessa classe) ricevono un valore di default (null, 0, false), per cui toString() restituisce null e println stampa "null". Il codice del corso va quindi corretto (int zz = 0;) prima di eseguirlo.

Generics

Il problema: contenitori di Object

Una coda che contiene Object accetta qualsiasi cosa, ma ogni elemento letto va riconvertito con un cast, e l'errore di tipo compare solo a tempo di esecuzione. Gli esempi del corso Coda/Main2 inseriscono per errore una Coda dentro la coda di stringhe e al prelievo il programma lancia ClassCastException. I generics spostano il controllo a tempo di compilazione.

Definizione (generics). Una classe o un metodo con uno o più parametri di tipo (class CodaG<T>): il tipo degli elementi è deciso da chi usa la classe (CodaG<Integer>), e il compilatore rifiuta gli usi incoerenti (ci.aggiungi("30") non compila).

java
import java.util.NoSuchElementException;

/** Coda circolare senza generics: contiene Object. */
class Coda {
    private final Object[] dati;
    private int testa, dim;

    Coda(int capacita) { dati = new Object[capacita]; }

    void aggiungi(Object o) {
        if (dim == dati.length) throw new IllegalStateException("coda piena");
        dati[(testa + dim) % dati.length] = o;
        dim++;
    }

    Object preleva() {
        if (dim == 0) throw new NoSuchElementException("coda vuota");
        Object r = dati[testa];
        dati[testa] = null;                 // niente riferimenti rimasti
        testa = (testa + 1) % dati.length;
        dim--;
        return r;
    }

    int dimensione() { return dim; }
}

/** La stessa coda con un parametro di tipo T. */
class CodaG<T> {
    private final T[] dati;
    private int testa, dim;

    @SuppressWarnings("unchecked")
    CodaG(int capacita) {
        dati = (T[]) new Object[capacita];  // cast non controllato: vedi la nota sulla type erasure
    }

    void aggiungi(T o) {
        if (dim == dati.length) throw new IllegalStateException("coda piena");
        dati[(testa + dim) % dati.length] = o;
        dim++;
    }

    T preleva() {
        if (dim == 0) throw new NoSuchElementException("coda vuota");
        T r = dati[testa];
        dati[testa] = null;
        testa = (testa + 1) % dati.length;
        dim--;
        return r;
    }

    int dimensione() { return dim; }
}

public class Contenitori {
    public static void main(String[] args) {
        // 1) senza generics: l'errore compare solo a tempo di esecuzione
        Coda c = new Coda(4);
        c.aggiungi("10");
        c.aggiungi("20");
        c.aggiungi(new Coda(1));            // compila senza problemi...
        int somma = 0;
        try {
            while (true) somma += Integer.parseInt((String) c.preleva());
        } catch (ClassCastException e) {    // ...ma esplode qui, al terzo prelievo
            System.out.println("Coda: ClassCastException dopo somma = " + somma);
        }

        // 2) con i generics: lo stesso errore e' un errore di compilazione
        CodaG<Integer> ci = new CodaG<>(4);
        ci.aggiungi(10);
        ci.aggiungi(20);
        // ci.aggiungi("30");               // errore: String non e' convertibile in Integer
        int s2 = 0;
        while (ci.dimensione() > 0) s2 += ci.preleva();   // niente cast, unboxing automatico
        System.out.println("CodaG<Integer>: somma = " + s2);

        // 3) type erasure: a tempo di esecuzione il parametro di tipo non esiste piu'
        CodaG<String> cs = new CodaG<>(1);
        System.out.println("stessa classe a runtime? " + (cs.getClass() == ci.getClass()));

        // 4) coda vuota: eccezione non controllata
        try {
            cs.preleva();
        } catch (NoSuchElementException e) {
            System.out.println("preleva() su coda vuota: " + e.getMessage());
        }
    }
}

Uscita:

Coda: ClassCastException dopo somma = 30
CodaG<Integer>: somma = 30
stessa classe a runtime? true
preleva() su coda vuota: coda vuota

Punti da ricordare:

Riflessione (reflection)

Definizione (riflessione). L'insieme di classi di java.lang e java.lang.reflect con cui un programma ispeziona e manipola classi, metodi e campi a tempo di esecuzione, anche senza conoscerli a tempo di compilazione.

Mappa delle classi:

classe rappresenta operazioni principali
Class<?> un tipo (classe, interfaccia, array) Class.forName("pacchetto.Nome"), getDeclaredConstructors(), getDeclaredMethods(), getDeclaredFields(), isArray(), componentType()
Constructor<?> un costruttore getParameterTypes(), newInstance(args)
Method un metodo getName(), getParameterTypes(), invoke(oggetto, args) (primo argomento null per i metodi statici)
Field un campo get(oggetto), set(oggetto, valore)

Punti chiave:

Eccezioni più frequenti:

eccezione quando
ClassNotFoundException il nome non corrisponde a nessuna classe nel classpath (il nome va completo di pacchetto: altro.Ciccia2)
NoSuchMethodException non esiste il costruttore o metodo con quei tipi di parametro (per esempio manca il costruttore senza parametri)
InstantiationException la classe è astratta o un'interfaccia
IllegalAccessException membro non accessibile (private, classe non pubblica)
InvocationTargetException il costruttore o il metodo invocato ha lanciato un'eccezione: la causa è in getCause()
IllegalArgumentException argomenti di numero o tipo sbagliati, oppure valore non assegnabile al campo
java
import java.io.IOException;
import java.lang.reflect.*;
import java.util.Arrays;

class Libro {
    static int istanze = 0;
    String titolo;

    static {
        System.out.println("  [static] la classe Libro e' stata caricata");
    }

    public Libro() { this("senza titolo"); }

    public Libro(String titolo) {
        this.titolo = titolo;
        istanze++;
    }

    public int lunghezzaTitolo() { return titolo.length(); }

    public String maiuscolo(String prefisso, int volte) {
        return (prefisso + titolo).repeat(volte).toUpperCase();
    }

    @Override public String toString() { return "Libro<" + titolo + ">"; }
}

class Segreto {
    private Segreto() { System.out.println("  [Segreto] costruttore privato eseguito"); }
}

class Fragile {
    public Fragile() throws IOException { throw new IOException("disco non pronto"); }
}

public class Riflessione {
    public static void main(String[] args) throws Exception {
        // 1) Class.forName carica la classe e ne esegue l'inizializzatore statico
        System.out.println("prima di forName");
        Class<?> c = Class.forName("Libro");
        System.out.println("dopo forName: " + c);

        // 2) elenco dei costruttori
        Constructor<?>[] costr = c.getDeclaredConstructors();
        Arrays.sort(costr, (a, b) -> a.getParameterCount() - b.getParameterCount());
        for (int i = 0; i < costr.length; i++) System.out.println("  #" + i + " " + costr[i]);

        // 3) creazione dinamica con il costruttore a un parametro String
        Constructor<?> k = c.getDeclaredConstructor(String.class);
        Object o = k.newInstance("Il nome della rosa");
        System.out.println("creato: " + o);

        // 4) invocazione dinamica di metodi (anche con parametri)
        Method m1 = c.getDeclaredMethod("lunghezzaTitolo");
        System.out.println("lunghezzaTitolo() = " + m1.invoke(o));
        Method m2 = c.getDeclaredMethod("maiuscolo", String.class, int.class);
        System.out.println("maiuscolo(\"x\",2) = " + m2.invoke(o, "x", 2));

        // 5) lettura e scrittura di un campo (stringa) e di uno statico
        Field f = c.getDeclaredField("titolo");
        f.set(o, "Il pendolo di Foucault");
        System.out.println("dopo f.set: " + o);
        Field n = c.getDeclaredField("istanze");
        System.out.println("istanze (campo statico, obj = null) = " + n.get(null));

        // 6) costruttore privato: lo trovo, ma non posso usarlo...
        Constructor<?> p = Class.forName("Segreto").getDeclaredConstructor();
        try {
            p.newInstance();
        } catch (IllegalAccessException e) {
            System.out.println("IllegalAccessException: " + e.getMessage());
        }
        // ...a meno di forzare l'accesso (la riflessione puo' aggirare private)
        p.setAccessible(true);
        p.newInstance();

        // 7) il costruttore lancia: l'eccezione arriva incapsulata
        try {
            Class.forName("Fragile").getDeclaredConstructor().newInstance();
        } catch (InvocationTargetException e) {
            System.out.println("InvocationTargetException, causa: " + e.getCause());
        }

        // 8) classe inesistente
        try {
            Class.forName("Inesistente");
        } catch (ClassNotFoundException e) {
            System.out.println("ClassNotFoundException: " + e.getMessage());
        }
    }
}

Uscita (l'ordine di getDeclaredConstructors non è garantito dalla JVM: il programma lo ordina per numero di parametri):

prima di forName
  [static] la classe Libro e' stata caricata
dopo forName: class Libro
  #0 public Libro()
  #1 public Libro(java.lang.String)
creato: Libro<Il nome della rosa>
lunghezzaTitolo() = 18
maiuscolo("x",2) = XIL NOME DELLA ROSAXIL NOME DELLA ROSA
dopo f.set: Libro<Il pendolo di Foucault>
istanze (campo statico, obj = null) = 1
IllegalAccessException: class Riflessione cannot access a member of class Segreto with modifiers "private"
  [Segreto] costruttore privato eseguito
InvocationTargetException, causa: java.io.IOException: disco non pronto
ClassNotFoundException: Inesistente

I caricatori interattivi del corso

Gli esempi Loader, Loader2... Loader7 (e quelli della dispensa) sono la stessa idea sviluppata per gradi: leggono da tastiera il nome di una classe e fanno qualcosa con essa.

programma cosa aggiunge
Loader Class.forName sul nome passato come argomento e stampa della classe
Loader2/Loader3 il nome si legge da tastiera in un ciclo; Loader3 crea l'oggetto (newInstance) e stampa toString()
Loader4 usa getDeclaredConstructor() e gestisce NoSuchMethodException (classe senza costruttore senza parametri) e InvocationTargetException
Loader5 elenca tutti i costruttori e fa scegliere; per ogni parametro chiama se stesso ricorsivamente (myInstantiate) per creare l'oggetto da passare
Loader6 dopo la creazione elenca i metodi (getDeclaredMethods) e ne esegue uno con invoke
Loader7 stampa il valore restituito; per i parametri array usa Array.newInstance(formals[i].componentType(), 0)
versioni della dispensa cambiare un campo con Field.set; scelta del metodo con nome e tipi dei parametri letti da tastiera

Le classi di prova mostrano i casi limite: Ciccia2 ha il costruttore private e un blocco statico che stampa al caricamento; Ciccia3 ha un costruttore che lancia IOException; Insta1 ha un costruttore che richiede un Insta2 (da qui la ricorsione); ArrayDentro ha un costruttore con Integer[]; Coccodrillo ha tre costruttori, di cui uno che lancia sempre.

Costi e usi

Domande d'esame

1. Un metodo Java riceve un oggetto e fa p = new Persona("X");. Il chiamante vede il cambiamento? E se il metodo fa p.setNome("X")? Traccia. Il passaggio è per valore del riferimento: la riassegnazione cambia solo la copia locale e il chiamante non vede nulla; p.setNome("X") modifica l'oggetto condiviso e il chiamante lo vede. Per le String (immutabili) anche l'equivalente di setNome non esiste: occorre restituire il nuovo valore.

2. Perché CodaG<String> e CodaG<Integer> hanno la stessa classe a runtime? Che cosa non si può fare per questo? Traccia. Type erasure: il compilatore verifica i tipi poi li cancella (un solo codice, a differenza dei template C++). Non si possono creare array di T (new T[n] si aggira con (T[]) new Object[n] e un cast non controllato), usare instanceof CodaG<String>, né avere parametri di tipo primitivo.

3. Scrivere in poche righe come creare un oggetto di classe Libro dal nome, con il costruttore che prende una String, e invocarne un metodo String titolo(). Quali eccezioni vanno gestite? Traccia. Class<?> c = Class.forName("Libro"); Object o = c.getDeclaredConstructor(String.class).newInstance("X"); Object r = c.getDeclaredMethod("titolo").invoke(o);. Eccezioni: ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException (la causa è l'eccezione lanciata dal costruttore o dal metodo).

Versione ripasso

  • Java e C: in Java ogni dato ha identità a runtime; il cast è controllato (ClassCastException), in C cambia solo il tipo visto dal compilatore. Lazy binding: le classi si caricano al primo uso (C: collegamento dinamico prima dell'esecuzione, -static per includerle).
  • Parametri: sempre per valore. Primitivi: copia, nessun effetto fuori. Oggetti: si copia il riferimento: riassegnare s = new ... non si vede fuori; s.campo = ... si vede; array: contenuto visibile; String immutabile (serve t = allunga(t)); non esiste swap(a, b) né parametro per riferimento (C++: int&). Variabili locali senza valore iniziale (errore might not have been initialized), i campi hanno il default.
passo di riassegna(s) chiamante metodo heap
ingresso → A → A A = Scheda(1)
dopo s = new Scheda(99) → A → B A e B
java
static void riassegna(Scheda s) { s = new Scheda(99); }     // il chiamante NON vede
static void modifica(Scheda s)  { s.copie = 99; }           // il chiamante vede
static void scambia(Scheda a, Scheda b) { Scheda t = a; a = b; b = t; }   // non scambia nulla fuori

Uscita del programma completo: n dopo incrementaPrimitivo = 5; dopo riassegna: Scheda(1); dopo modifica: Scheda(99); dopo scambia: a = Scheda(1), b = Scheda(2); array dopo riempi: [7, 7, 7]; t dopo allunga (valore scartato) = ciao; t dopo t = allunga(t) = ciao!.

  • Generics: class CodaG<T>; l'errore di tipo diventa errore di compilazione; niente cast. Erasure: a runtime CodaG<String> e CodaG<Integer> sono la stessa classe; niente primitivi (Integer, autoboxing; == tra Integer confronta riferimenti, 127 sì, 128 no); niente new T[n] ((T[]) new Object[n] + @SuppressWarnings("unchecked")); niente instanceof CodaG<String>. Non sono template C++ (un solo codice contro una copia per tipo). Evitare i tipi grezzi (heap pollution, errore alla lettura). <T extends Comparable<T>>: vincolo; ? extends Number: sola lettura di qualunque sottotipo.
java
CodaG<Integer> ci = new CodaG<>(4);
ci.aggiungi(10);                     // ci.aggiungi("30") non compila
int s = ci.preleva();                // niente cast: unboxing automatico
  • Riflessione: Class.forName("pacchetto.Nome") carica e inizializza (esegue static { }); getDeclaredConstructors/Methods/Fields; Constructor.newInstance(args); Method.invoke(obj, args) (null per gli statici); Field.get/set. Class.newInstance() è deprecato. Metodi identificati da nome + tipi (int.class diverso da Integer.class). getDeclaredConstructors non ha ordine garantito.
java
Class<?> c = Class.forName("Libro");
Object o = c.getDeclaredConstructor(String.class).newInstance("Il nome della rosa");
Object n = c.getDeclaredMethod("lunghezzaTitolo").invoke(o);
eccezione quando
ClassNotFoundException nome inesistente (serve il nome completo di pacchetto)
NoSuchMethodException costruttore/metodo con quei tipi non c'è
InstantiationException classe astratta o interfaccia
IllegalAccessException private o classe non pubblica
InvocationTargetException il codice invocato ha lanciato: causa in getCause()

Cosa stampa il programma di riflessione (nell'ordine): prima di forName, poi [static] la classe Libro e' stata caricata (l'inizializzatore statico gira dentro forName), i due costruttori, creato: Libro<Il nome della rosa>, lunghezzaTitolo() = 18, maiuscolo("x",2) = XIL NOME DELLA ROSAXIL NOME DELLA ROSA, istanze (campo statico, obj = null) = 1, IllegalAccessException sul costruttore privato, il costruttore privato eseguito dopo setAccessible(true), InvocationTargetException, causa: java.io.IOException: disco non pronto, ClassNotFoundException: Inesistente.

Esercizi su questo argomento

Teoria collegata